Case: I'm building a forum using Laravel's Authorization as a backbone using policies. Examples of checks I run are stuff like #can('view', $forum), and #can('reply', $topic), Gate::allows('create_topic', $forum) etc. These checks basically checks if the users role has that permission for the specific forum, topic or post. That way I can give roles very specific permissions for each forum in my application.
The issue is that all of these checks go through the Gate class, specifically a method called raw() which in its first line does this:
if (! $user = $this->resolveUser()) {
return false;
}
This presents an issue when dealing with forums. Guests to my application should also be allowed to view my forum, however as you can see from the code above, Laravels Gate class automatically returns false if the user is not logged in.
I need to be able to trigger my policies even if there is no user. Say in my ForumPolicy#view method, I do if(User::guest() && $forum->hasGuestViewAccess()) { return true }
But as you can see, this method will never trigger.
Is there a way for me to still use Laravel's authorization feature with guest users?
I'm not aware of a super natural way to accomplish this, but if it's necessary, you could change the gate's user resolver, which is responsible for finding users (by default it reads from Auth::user()).
Since the resolver is protected and has no setters, you'll need to modify it on creation. The gate is instantiated in Laravel's AuthServiceProvider. You can extend this class and replace the reference to it in the app.providers config with your subclass.
It's going to be up to you what kind of guest object to return (as long as it's truthy), but I'd probably use something like an empty User model:
protected function registerAccessGate()
{
$this->app->singleton(GateContract::class, function ($app) {
return new Gate($app, function () use ($app) {
$user = $app['auth']->user();
if ($user) {
return $user;
}
return new \App\User;
});
});
}
You could go a step further and set a special property on it like $user->isGuest, or even define a special guest class or constant.
Alternatively you could adjust your process at the Auth level so that all logged-out sessions are wrapped in a call to Auth::setUser($guestUserObject).
I just released a package that allows permission logic to be applied to guest users. It slightly modifies Laravel's Authorization to return a Guest object instead of null when no user is resolved. Also every authorization check now makes it to the Gate instead of failing authorization instantly because there isn't an authenticated user.
Related
I have this test:
public function test_user_can_access_the_application_page(
{
$user=[
'email'=>'user#user.com',
'password'=>'user1234',
];
$response=$this->call('POST','/login',$user);
$this->assertAuthenticated();
$response->assertStatus(302)
->assertRedirect('/dashboard')
->assertLocation('/dashboard');
$response=$this->call('GET','/application/index');
$response->assertLocation('/application/index');
}
After I log in, it directs me to the dashboard ok until now, but if I want to access the other page after that, I cant. This error comes up.
Expected :'http://mock.test/application/index'
Actual :'http://mock.test'
Aren't multiple calls allowed in the same test, or is another way to access other pages after login?
(Note: It's not possible to use factories for the actingAs so I need to login).
If you can't use factories for actingAs, then you should try with cookie.
Look at the https://github.com/firebase/php-jwt library.
I guess you will need to call the function as an user, since you can only access it logged in. Laravel provides the actingAs() method for such cases.
https://laravel.com/docs/7.x/http-tests#session-and-authentication
You can create a random User who has the permission to log into your app or take a seeded one and call the function acting as the chosen User.
$response=$this->actingAs($user)->call('GET','/application/index');
If you call it without actingAs(), your middleware will redirect you back to the login or home screen (what you defined in the LoginController ).
In my opinion this test case should have its own testing method. I recommend using a test method per route or per use case. It makes your tests clearly arranged and easy to understand.
If you want to be authenticated, the easiest way is to have PHPUnit simulate authentication using the actingAs() method.
This method makes the user authenticated, so you wouldn't want to test the login method with it. You should write your login tests separate from testing the other pages.
To answer your question, yes you can make multiple requests in the same test, but in this case linking the login test to the 'application/index' page likely does not make much sense.
public function test_the_user_can_login()
{
$user = [
'email'=>'user#user.com',
'password'=>'user1234',
];
$response = $this->call('POST','/login',$user);
$this->assertAuthenticated();
$response->assertStatus(302)
->assertRedirect('/dashboard')
->assertLocation('/dashboard');
}
public function test_user_can_access_the_application_page()
{
$user = User::where($email, "user#user.com")->first();
$response = $this->actingAs($user)
->call('GET','/application/index');
$response->assertLocation('/application/index');
}
I experienced that in laravel 8 I use comment #test and involved second test!!! I mean if you use two function for test you must us #test that php artisan test, test both of them.
The user can log in, but i need his id to show his profile. Since i'm working in symfony 4, the many possible answers i've found were obsolete.
https://symfony.com/doc/current/security.html#retrieving-the-user-object
After authentication, the User object of the current user can be accessed via the getUser() shortcut (which uses the security.token_storage service). From inside a controller, this will look like:
public function index()
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
$user = $this->getUser();
}
The user will be an object and the class of that object will depend on your user provider. But, you didn't tell us what user provider you are using, so I have no way of telling you, beyond this, how to get to the id itself.
One other way to get the user:
An alternative way to get the current user in a controller is to type-hint the controller argument with UserInterface (and default it to null if being logged-in is optional):
use Symfony\Component\Security\Core\User\UserInterface\UserInterface;
public function indexAction(UserInterface $user = null)
{
// $user is null when not logged-in or anon.
}
This is only recommended for experienced developers who don't extend from the Symfony base controller and don't use the ControllerTrait either. Otherwise, it's recommended to keep using the getUser() shortcut.
My team has an API we wrote in PHP using the Slim Framework. It's being consumed by a web app and a third party mobile app.
We use the standard OAuth 2 workflow to provide access to our resources. We can see if someone is a sending a valid access token along with the API request and that part of the flow makes sense.
The stumbling block we're running into is how to most efficiently authorize access to a resource depending on the permissions of the user associated with the access token.
For the sake of simplicity let's say that we just want to ensure that the owner of the resource in question matches the owner of the access token. To me that seems like something that a route middleware would handle, for every request, before processing the request make sure that the resource owner ID matches that of the access token.
The problem in my mind is that resource permissions aren't necessarily consistent across all routes, a route isn't necessarily going to have an ID in the same section of the URI, it might not have an ID in the URI at all, etc. etc.
My current solution has been to have an authorization utility class that takes in an email and checks it against the user that's currently "logged in" (token owner).
class Authorization() {
checkResourcePermissions($email) {
if (loggedInUser->email == $email) {
return true;
}
}
}
Obviously this is a simplification, but what this means is that since a route middleware won't have the context of a request until that request goes through I will need to call this authorization method inside of every API route, responding with an error if necessary, etc. Our API is fairly large, essentially boiling this down to a large amount of copy and paste which always makes me very nervous.
What am I missing?
I'd suggest going the way using a 'normal' MiddleWare:
First, let's assume you have a class 'user', where you implement your logic to access various types of resources or has specific permissions, given only the $app. It can access the request and get any information from a the HTTP request necessary to identify a single user.
class User{
private $userid=false;
public function __construct($app){
$this->app=$app;
$users_mail=$this->app->request->post('email');
//SELECT user_id FROM USERSTABLE WHERE email = $users_mail
//$this->userid=user_id
}
public function ensureHasAccess($resourceID,$redirect='/noPermission'){
if(!/*some DB Query which returns true if this user may access the given $resourceID*/)
$this->app->redirect($redirect)
}
public function ensureHasRole($role) {
//some other permission checking, like ensureHasAccess();
}
public function ensureIsUser(){
//do Something
}
}
Next, you'll need a MiddleWare which is run before the route is dispatched, and creates an User-Object:
class UserPermissionMiddleware extends \Slim\Middleware{
public function call(){
$this->app->request->user = new User($app);
$this->next->call();
}
}
Then, simply add this Middleware:
$app->add(new UserPermissionMiddleware());
Now, you can modify every route to check the access to a resource, while the resource id (of course) still needs to be supplied by hand, according to the route-specific code:
$app->get('/resource/:id',function($resourceId) use ($app){
$app->request->user->ensureHasAccess($resourceId);
//deliver resource
});
If the user is not allowed to access this resource, the whole process is interrupted (Note that redirect() is implemented using Exceptions!) and another route is executed. You'll still need some code in every single Route, but if there's no consistent way to determine the resource to be acccessed, there'll be no way around this.
Hope this helps you so far. If you have any Questions, feel free to ask!
Is there a way to restrict access to specific routes aka action/method of controller in Symfony based on user?
I am implementing FOSUserBundle for user management and it has roles for defining permission that works well if I have user with defined roles but if I want to restrict the page based on users I have to create role for every routes or are there any better approach.
I have looked into ACL and its perfect but I don't find solution for my case or am I missing something there.
Looking for some help and ideas.
Updates
#AJ Cerqueti - Answer can be quick fix but I am looking for better approach if any.
To be more specific is it possible to assign access permission for routes to user using ACL or some other better approach.
SYMFONY >=2.6
create a security voter
How to Use Voters to Check User Permissions
Now change the access decision strategy accordingly with the docs
SYMFONY <=2.5
For simple needs like this you can create a security voter that fit exactly what you need (ACL's are usually used for complex needs also due to the not easy implementation).
Then you can use the voter in your controller like described in the docs:
// get a Post instance
$post = ...;
// keep in mind, this will call all registered security voters
if (false === $this->get('security.authorization_checker')->isGranted('view', $post)) {
throw new AccessDeniedException('Unauthorised access!');
}
Read also How to Use Voters to Check User Permissions
UPDATE BASED ON COMMENT:
Maybe will be better to know on how many routes you want to add this behavior but in any case (and if you want to avoid to add in every controller the #AJCerqueti code) you can use a Voter like in this simple example:
Voter Class:
// src/AppBundle/Security/Authorization/Voter/RouteVoter.php
namespace AppBundle\Security\Authorization\Voter;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
class RouteVoter implements VoterInterface
{
private $routes;
public function __construct(array $routes = array())
{
$this->routes = $routes;
}
public function supportsAttribute($attribute)
{
// you won't check against a user attribute, so return true
return true;
}
public function supportsClass($class)
{
// your voter supports all type of token classes, so return true
return true;
}
public function vote(TokenInterface $token, $object, array $attributes)
{
// get get allowed routes from current logged in user
$userRoutes = $token->getUser()->getRoutes();
// implement as you want the checks and return the related voter constant as below
if (...) {# your implementation
return VoterInterface::ACCESS_DENIED;
}
return VoterInterface::ACCESS_ABSTAIN;
}
}
Register the Voter:
<service id="security.access.route_voter"
class="AppBundle\Security\Authorization\Voter\RouteVoter" public="false">
<argument type="collection">
<argument>route_one</argument>
<argument>route_two</argument>
</argument>
<tag name="security.voter" />
Now change the access decision strategy accordingly with the docs.
Can this fit your needs?
Agree with previous answers that ACLs, Voters or some sort or role-based solution is definitely the best practice approach, but for this fringe case, would suggest extending your FOSUser to add a 'slug' field, and then check on that:
if('accessible_slug' !== $this->get('security.context')->getToken()->getUser()->getSlug()) {
throw new AccessDeniedException()
}
This means setting up a slug for groups of controllers/actions, and setting them to the user. Similar to roles, but without quite as much overhead. Still prefer Voters and some sort of role hierarchy, but hope this helps.
You can use AccessDeniedException of Symfony2
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
Then check logged in User By,
if (false === $this->get('security.authorization_checker')->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedException();
} else {
Continue;
}
In app/config/security.yml there is a section access_control:. There you can define access restrictions for specific paths, eg.
- { path: ^/faq/admin, roles: ROLE_FAQ_ADMIN }
path argument is a regex so the above entry will restrict access to any path starting with /faq/admin, eg. /faq/admin/show-something, /faq/admin/show-something-else etc. Only users with specified role will have access to those paths. For other users AccessDeniedException will be thrown with HTTP code 403.
There is no need to change code in actions inside controllers.
I saw some codes on internet which in order to check the permissions to access a concrete action, they use the Configure::read function in this way:
public function action1(){
if(!Configure::read('isAdmin')){
$this->redirect(array('controller' => 'depots', 'action' => 'status'));
}
//whatever
}
I was wondering, which is the difference between using Configure::read and Configure:write for this purpose and using $this->Session->read() and $this->Session->write()?
Which is a better way to check it?
Thanks.
Using the AuthComponent
If you make use of the built-in AuthComponent, CakePHP will store details of the currently logged-in user inside the session.
Getting properties of the currently logged-in User
Once logged in, you can access the information of the Used (e.g. role_id) via the AuthComponent. This can be done anywhere (also inside your Views or Models if desired);
For example;
if (123 === AuthComponent::user('role_id')) {
debug('hello admin user');
}
Or, inside a Controller:
if (123 === $this->Auth->user('role_id')) {
debug('hello admin user');
}
Accessing the logged in user
However, to dont have to repeat the group-id everywhere, it's best to creat a method for this (e.g inside your AppController);
/**
* Checks if the currently logged in user is an admin
*
* #return bool true if the current user is an admin
*/
protected function isAdmin()
{
// probably best to make the id configurable (Configure::write())?
return (123 === $this->Auth->user('role_id'));
}
Access control
To use a 'simple' authorisation, you can create your own isAuthorized() action in your Controller, which will allow you to block access to specific actions, based on the properties of the currently logged-in user;
Using ControllerAuthorize
I can't see why you would put the user role in the Configure array, as it is intended to contain application wide settings.
Personaly I have a table in my database that contain the roles. Although some roles may be added to it, there are some that I never modify (typically the administrator role).
This allows me to store its value as an application parameter in Configure and check for it later:
bootstrap.php
Configure :: write('administrator.role_id', 1);
TestController:
if($this->Auth->user('role_id') == Configure :: read('administrator.role_id'))
{
//do things specific to admin role
}
That said if the user role is stored dynamically in one way or another in Configure, it could probably work as well, but that's probably not the more elegant solution.