Why optional params passed while removed params fails in policy testing? - laravel

So, I have created a test for my app, the model is Subscription which is the index and show endpoint should be publicly accessible.
I created a Resource Controller to handle I/O from the client and a Policy to handle the authorization but here, I found something that seems kinda odd.
Inside the controller, I registered the policy on the constructor, like so:
public function __construct()
{
$this->middleware('auth:api')->except(['index', 'show']);
$this->authorizeResource(Subscription::class, 'subscription');
}
And then in the policy class, I modified the default generated methods like so:
/**
* Determine whether the user can view any models.
*
* #param \App\Models\User $user
* #return mixed
*/
public function viewAny(?User $user) // <-- notice here I make it optional, the original was required (without "?" mark).
{
return true; // publicly visible
}
When I run the test, it passed.
public function testSubscriptionIndexArePubliclyAccessible()
{
$subscriptions = Subscription::factory(10)->create()->toArray();
$response = $this->get(route('subscriptions.index'));
$response->assertOk();
$response->assertExactJson($subscriptions);
}
However, if I completely remove the User $user param from the method, the test would fail.
public function viewAny() <-- if I do this, the test fail. Saying that "this action is unauthorized".
{
return true; // publicly visible
}
So.. Why is this happen?

There are checks happening before a policy method or gate ability are called. One check is if the policy method can be called with a user, canBeCalledWithUser. This will check if there is an auth user and return true, if not it does other checks. The next check is if the method allows guest users, methodAllowsGuests, which will use reflection to get the parameter for that method and see if it has a type and is nullable, but there are no parameters so it returns false. So you end up with it not calling the method and treating it more like it doesn't exist, which is always false in terms of authorization checks.
https://github.com/laravel/framework/blob/8.x/src/Illuminate/Auth/Access/Gate.php#L371 #raw -> callAuthCallback -> resolveAuthCallback
https://github.com/laravel/framework/blob/8.x/src/Illuminate/Auth/Access/Gate.php#L530 #resolveAuthCallback
https://github.com/laravel/framework/blob/8.x/src/Illuminate/Auth/Access/Gate.php#L390 #canBeCalledWithUser
https://github.com/laravel/framework/blob/8.x/src/Illuminate/Auth/Access/Gate.php#L416 #methodAllowsGuests
https://github.com/laravel/framework/blob/8.x/src/Illuminate/Auth/Access/Gate.php#L456 #parameterAllowsGuests - it does not make it to this method call

Related

How to assert $next in Laravel Middleware

Lavarvel application running 7.10
I have a middleware that gets called on several routes. It check the user has the right relationships in place to get to the desired destination (that's not important).
I want a test that simply asserts that the user gets to the $next($request) in the middleware. I could of course just hit one of the end points, and assert something about that which would in turn validate the middleware, but all the routes hit external apis to get some other data which I could mock in my tests, which all seem pretty ugly just to test a middleware class.
I my test I can factory up the relevant relations etc, so whenever the middleware is called it should allow me straight in.
If I do this...
/** #test */
public function middleware_successful_test()
{
$user = factory(User::class)->create();
// some other factoried user relations here
$this->actingAs($user);
$request = new Request;
$middleware = app(MiddlewareClass::class);
$result = $middleware->handle($request, function ($req) {});
// don't know what assert here. $result is basically empty
}
Any tips, advise welcome.
Normally i would not unit test middlewares, but test it as a feature.
For your case i would utilize that you provide the next closure and you get the result. Return true and assert it is what is returned and therefor the closure has been called.
$result = $middleware->handle($request, function ($req) { return true;});
$this->assertSame(true, $result);

Routing & pretty URL

I am looking to create prettier URLs, and I'm having issues creating a valid route:
Let's say I have the following page http://localhost/app/account/5/edit.
This works fine with Route::get('account/{account}', 'AccountController#edit');
If I modify the Account Model and modify getRouteKeyName to return 'name', I am able to (with the same Route from above) access the following link: http://localhost/app/account/randomName/edit
The thing is, I am interested in a slightly different route, which is: http://localhost/app/account/randomName-5/edit
If I create a route Route::get('/accounts/{ignore}-{account}/edit', 'AccountController#edit'), it will fail as the first argument sent to edit is string and not an instance of Account. This can easily be fixed by modifying edit(Account $ac) to edit($ignored, Account $ac);... but it feels like cheating.
Is there a way to to get the route to ignore the first {block}?
According to the docs, you can customize your resolution logic for route model binding.
In this scenario, you can do something like this in App\Providers\RouteServiceProvider:
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
parent::boot();
Route::bind('accountNameWithId', function ($value) {
list($accountName, $accountId) = explode('-', $value);
return App\Account::whereKey($accountId)
->where('name', $accountName)
->firstOrFail();
});
}
Then you can redefine your route like this:
Route::get('account/{accountNameWithId}', 'AccountController#edit');

Laravel 5.7: prevent auto load injection in controller constructor

I have a HomeController with his constructor that takes a Guzzle instance.
/**
* Create a new controller instance.
*
* #param \GuzzleHttp\Client|null $client
*
* #return void
*/
public function __construct(Client $client = null)
{
$this->middleware('auth');
$this->middleware('user.settings');
if ($client === null) {
$param = [
'base_uri' => 'http://httpbin.org/',
'defaults' => [
'exceptions' => false,
'verify' => false
]
];
$client = new Client($param);
}
$this->setClient($client);
}
I would use via __constructor() to be able to mock it in tests.
My issues is that Laravel automatically auto-load the injection and the Guzzle Client injected has blank defaults (and cannot anymore edit it). In other words: at first call of HomeController Client is not null. And I need as null.
How can I stop this behaviour (only for the __construct() for HomeController)? I really use the DI in every part of my webapp.
EDIT
I just find that if I don't type-hints the Client, of course Laravel cannot auto-load. Is this the right mode to work?
New constructor:
public function __construct($client = null)
Thank you
I had a simular situation when testing apis. I ended up binding an instance of GuzzleClient to the service container (see documentation). Something like:
$this->app->instance('GuzzleHttp\Client', new MockClient);
To successfully mock the instance, I then checked to see whether or not it had a certain property value (in my case base_url being set). That determined whether or not the instance was a test as base_url would be set.
Along side this method, GuzzleHttp\Client does have a MockHandler you may want to explore. This can be used to fake response bodies, headers and status codes.

How do I access non id property of request inside form requests?

I want to access route request parameters inside laravel form requests authorize. I cant find an example describing this.
// Works fine when you want id
dd($this->route('myResourceName'));
// How to do when I want something else???
dd($this->route('anotherAttribute'));
// Above give null probably because it is a resourceful controller
On a side note, I dont understand this design, whats the point?
$this->route('anyAttribute') would be the easiest, right?
Edit: more extensive example
class UpdateSlotAPIRequest extends APIRequest
{
public function __construct(){
parent::__construct();
$this->slot = Slot::find($this->route('slot'));
$this->access_token = $this->route('access_token'); // this is not working!
}
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
// If administrator is logged in all is good.
// If slot is free its ok.
// If its not free but you provide good access_token its also fine.
return Auth::check() || $this->slot->isAvailable() || $this->slot->isValidAccessToken($this->access_token);
}
...
```
$access_token = request()->input('access_token');
Found it in https://laravel.com/docs/5.4/helpers

Laravel 5.3 Passing AuthorizationException a message when a Policy fails

I'm trying to find a clean way to override the AuthorizationException to take a dynamic string that can be passed back when a Policy fails.
Things I know I can do are:
Wrap the Policy in the Controller with a try-catch, then rethrow a custom exception that takes a specific string, which seems a bit verbose
abort(403, '...') in the Policy prior to returning, which seems a bit hacky since policies are already doing the work
and then in /Exceptions/Handler::render I can send back the response as JSON
Is there a nicer way to do this to get a message in the response of a policy failure? Or is 1 or 2 my best choices.
I noticed if you throw AuthorizationException($message) in a policy using Laravel's exception it jumps you out of the policy, but continues execution in the controller, and doesn't progress to Handler::render. Which I'm assuming this is them handling the exception somehow, but I couldn't find where they were doing it... so if anyone finds where this is happening I'd still like to know.
If you create your own AuthorizationException and throw it, it will stop execution as expected, and drop into Handler::render so I ended up adding this method to my policy:
use App\Exceptions\AuthorizationException;
// ... removed for brevity
private function throwExceptionIfNotPermitted(bool $hasPermission = false, bool $allowExceptions = false, $exceptionMessage = null): bool
{
// Only throw when a message is provided, or use the default
// behaviour provided by policies
if (!$hasPermission && $allowExceptions && !is_null($exceptionMessage)) {
throw new \App\Exceptions\AuthorizationException($exceptionMessage);
}
return $hasPermission;
}
New exception for throwing in policies only in \App\Exceptions:
namespace App\Exceptions;
use Exception;
/**
* The AuthorizationException class is used by policies where authorization has
* failed, and a message is required to indicate the type of failure.
* ---
* NOTE: For consistency and clarity with the framework the exception was named
* for the similarly named exception provided by Laravel that does not stop
* execution when thrown in a policy due to internal handling of the
* exception.
*/
class AuthorizationException extends Exception
{
private $statusCode = 403;
public function __construct($message = null, \Exception $previous = null, $code = 0)
{
parent::__construct($message, $code, $previous);
}
public function getStatusCode()
{
return $this->statusCode;
}
}
Handle the exception and provide the message in a JSON response in Handler::render():
public function render($request, Exception $exception)
{
if ($exception instanceof AuthorizationException && $request->expectsJson()) {
return response()->json([
'message' => $exception->getMessage()
], $exception->getStatusCode());
}
return parent::render($request, $exception);
}
and I also removed it from being logged in Handler::report.
What I found was not "passing" a custom message to authorize, just defining a custom message in the policy it selfs, so, for example, if you have the method "canUseIt", in your UserPolicy, like the following:
public function canUseIt(User $user, MachineGun $machineGun)
{
if ($user->isChuckNorris()) {
return true;
}
return false;
}
You can change it and do something like this:
public function canUseIt(User $user, MachineGun $machineGun)
{
if ($user->isChuckNorris()) {
return true;
}
$this->deny('Sorry man, you are not Chuck Norris');
}
It uses the deny() method from the HandlesAuthorization trait.
Then when you use it like $this->authorize('canUseIt', $user) and it fails, it will return a 403 HTTP error code with the message "Sorry man, you are not Chuck Norris".
Laravel does have an option to pass arguments to customize the errors in the authorize() method of a Controller Class accessed through the Gate Class's implementation of the GateContract made available by the Gate Facade.
However, it seems that they forgot to pass those arguments to the allow()/deny() methods responsible for returning error messages, implemented in the HandlesAuthorization Trait.
You need to pass those arguments by following these steps:
Modify the authorize method in the vendor/laravel/framework/src/Illuminate/Auth/Access/Gate.php file
public function authorize($ability, $arguments = []) {
$result = $this->raw($ability, $arguments);
if ($result instanceof Response) {
return $result;
}
return $result ? $this->allow() : $this->deny($arguments);
}
Call authorize from the controller with an extra argument, ie: your custom $message -
$message = "You can not delete this comment!";
$response = $this->authorize('delete', $message);
I have made a pull request to fix this, hopefully someone will merge it soon.
I think the best way to think about Policies is they are simply a way to split controller logic, and move all authorization related logic to a separate file. Thus abort(403, 'message') is the right way to do this, in most cases.
The only downside is you may want some policies to be 'pure' logic for use in business logic only, and thus not to have any response control. They could be kept separate, and a commenting system could be used distinguish them.

Resources