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.
Related
Since I started logging exceptions on a production site I'm noticing a lot of them, especially 404s, more than I would expect for a site with barely any traffic, and I'd like to get to the bottom of whether they're genuine users or just bots. To help with this, I want to capture the URL that the user was trying to visit before being redirected to the 404 route, so I can keep track of which non-existent routes are being mistakenly hit. I think I'm correct in assuming this URL should be available in the request, and that I just need to store the request and pass it through to the exception.
What's the best way to do this in Laravel 8 onwards?
Catch 404 exceptions in Handler(App\Exceptions\Handler).
If you see Rendering Exceptions
By default, the Laravel exception handler will convert exceptions into
an HTTP response for you. However, you are free to register a custom
rendering closure for exceptions of a given type. You may accomplish
this via the renderable method of your exception handler.
The closure passed to the renderable method should return an instance
of Illuminate\Http\Response, which may be generated via the response
helper. Laravel will deduce what type of exception the closure renders
by examining the type-hint of the closure:
so in the register method,call renderable
public function register()
{
$this->renderable(function (NotFoundHttpException $e, $request) {
Log::alert("404",[
"fullUrl"=>$request->fullUrl(),
"path"=>$request->path(),
"message" =>$e->getMessage()
]);
return response()->view('errors.404', [], $e->getStatusCode());
});
}
Also, don't forget to import and use
Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
EDIT: just a few hours of using this solution in production, with the benefit of the newly-added request URLs, has confirmed for me just how many of these HTTP 4xx errors are junk - mostly automated bots and maybe a few script kiddies trying common routes. For this reason I've added some logic to ignore 404 and 405 errors, and may still add others that mostly contribute noise to the logfile.
This was harder than it should have been, but this is the solution I'm currently using to log the request with all exceptions. It's probably not the cleanest way to do it, but it works perfectly for my needs. Thanks to John Lobo's answer for pointing me in the right direction.
It works by inspecting each instance of the Exception class and using PHP's instanceof to check whether it's a HTTP exception or not. If it is, it gets logged with the request URL and returns a view with a status code. If it's a generic non-HTTP exception, it gets logged with the request URL and returns another view with no status code (or you can keep the default exception behaviour by removing the return block, which renders a blank screen in production).
public function register()
{
$this->renderable(function (Exception $exception, $request) {
$url = $request->fullUrl();
if ($exception instanceof HttpException) {
$status = $exception->getStatusCode();
// Do not log HTTP 404 and 405s errors for reasons that will
// become apparent after a few hours of logging 404s and 405s
if(($status !== 404) && ($status !== 405)) {
Log::warning("Error $status occurred when trying to visit $url. Received the following message: " . $exception->getMessage());
}
return response()->view("errors.error", [
"exception" => $exception,
"status" => $status
],
$status
);
} else {
$status = $exception->getCode();
Log::warning("Exception $status occurred when trying to visit $url. Received the following message: " . $exception->getMessage());
return response()->view("errors.exception", [
"exception" => $exception,
"status" => $status
]);
}
});
// Optionally suppress all Laravel's default logging for exceptions, so only your own logs go to the logfile
$this->reportable(function (Exception $e) {
})->stop();
}
I've checked the docs and SO but when I do the following I get the error Maximum execution time of 30 seconds exceeded.
In my custom form request I have:
public function withValidator($validator)
{
$validator->after(function ($validator) {
if ($validator->fails()) {
session()->flash('type', 'xyz');
}
});
}
As soon as I removed the if statement the max execution error goes away.
How can I add some data to session if validation fails?
The reason is because you are calling fail in the after callaback and this is doing an infinite recursive call. Let me explain:
if you dig in the source code here is the fails implementation:
public function fails()
{
return ! $this->passes();
}
Here is our interesting part of the passes() method:
public function passes()
{
//.....
//once the validation fails call the callback
foreach ($this->after as $after) {
$after();
}
return $this->messages->isEmpty();
}
So if in the after() callback you are calling the fail() method then the fail() method will call the after() callabck etc.
As said in laravel doc
withValidator allows you to call any of its methods before the validation rules are actually evaluated
So, you cannot call fails() method inside withValidator method. Because it performs validation and cannot be called in a method that adds a rule that is ought to perform before validation.
You should define a validator as said here in your controller, then call fails() method on it.
$validator = Validator::make(...);
if ($validator->fails()) {
session()->flash('type', 'xyz');
}
because what has mentioned in Doc was:
call any of its methods before the validation rules are actually
evaluated
your code going through an infinite loop by recursive calling himself on calling $validator->fails()
If you want to add some data when the validation fails using after validation hook, you can try to check if the error bag has any messages and if it has then flash the requisite data to session
public function withValidator($validator)
{
$validator->after(function($validator) {
if($validator->errors()->count('messages')) {
session()->flash('type', 'xyz');
}
});
}
I'm handling laravel exceptions such as NotFoundHttpException and UnauthorizedException by type-checking exceptions in the exception handler's render method, making a lot of instanceof checks which in my opinion violates OCP (open for extension closed for modification) principle.
The documentaion solves this by using renderable exceptions (https://laravel.com/docs/6.x/errors#renderable-exceptions) that must be thrown from the controller (or any where) which I don't want, I want to catch laravel exceptions' such as ModelNotFoundException in the handler class and return my custom response in a clean way,in other words, I'm looking for a clean way to handle laravel exceptions without throwing the exception from the controller.
If it is about OCP only, you can use a chain of responsibility design pattern, you can create a class (which is open-for-extension closed-for-modifications), that has a "next" field, which is from the same class type, and make the conversions you want like that:
interface ErrorHandler {
function handleError($request);
}
abstract class NodeInErrorHandlerChain {
private $next; // It should have the same `NodeInErrorHandlerChain` type
public NodeInErrorHandlerChain($next) {
$this->next = $next;
}
abstract protected function currentHandler($exception, $request);
public function handle($exception, $request) {
$current = currentHandler($exception, $request);
return $current != null ? $current :
($next == null ? null : $next->handle($exception, $request));
}
}
Then implement it as follows:
class ModelNotFoundNode extends NodeInErrorHandlerChain {
protected function currentHandler($exception, $request) {
if($exception instanceof ModelNotFoundException) {
return ModelNotFoundHandler($request); // This should be easy to implement
}
}
}
So, now you check on ModelNotFoundException only, if you want to check on other types, create the ModelNotFoundNode with $next not equal to null, but rather equal to for example ValidationException (you would implement it the same way), so adding any other exception of those would be just making another class that extends NodeInErrorHandlerChain, and make it the $next to the previous last element of the chain, when you create the chain (for example in the Provider which provides dependency injection in your app).
$this->app->singleton(NodeInErrorHandlerChain::class, function ($app) {
return new ModelNotFoundNode(ValidationExceptionNode(null));
});
I have a Season Resource model with a field named active.
The requirement is to disable deletion for a season with an active status.
I have created an Observer for the season model to watch deleting an event. From this function, I can block the delete in case active is true.
But the issue is with the error message; is there any way to add an error message to session flash from the Observer class?
<?php
public function deleting(Season $season)
{
if($season->active_season)
{
Log::info('Sorry, this season can`t be deleted.
There must be at least one active season.');
}
return false;
}
This is not tested but I was able to achieve something like that in a previous project:
use Illuminate\Validation\ValidationException;
class AbcObserver
{
public function creating(Abc $abc)
{
if ($abc->details != 'test') {
throw ValidationException::withMessages(['details' => 'This is not valid.']);
}
}
}
You can use the Exception class. I tested it in a Nova action and it throws the toast error notification.
use Exception;
throw new Exception('Error message here ...');
// Or
throw_if(
$validator->fails(), // or any true boolean
Exception::class,
'Error message here ...'
);
I don't know how to flash the error message.
But since the requirement is to disable deletion for a season with an active status, I'm suggesting to use policy which won't display the delete icon when doesn't match the condition.
class SeasonPolicy {
...
public function delete(User $user, Season $season) {
if($season->active_season) {
return false;
}
return true;
}
}
and register the policy in AuthServiceProvider.
Note:
Undefined Policy Methods
If a policy exists but is missing a method for a particular action,
the user will not be allowed to perform that action. So, if you have
defined a policy, don't forget to define all of its relevant
authorization methods.
I've got a Controller.php whose show($id) method is hit by a route.
public function show($id)
{
// fetch a couple attributes from the request ...
$this->checkEverythingIsOk($attributes);
// ... return the requested resource.
return $response;
}
Now, in checkEverythingIsOk(), I perform some validation and authorization stuff. These checks are common to several routes within the same controller, so I'd like to extract these checks and call the method everytime I need to perform the same operations.
The problem is, I'm unable to send some responses from this method:
private function checkEverythingIsOk($attributes)
{
if (checkSomething()) {
return response()->json('Something went wrong'); // this does not work - it will return, but the response won't be sent.
}
// more checks...
return response()->callAResponseMacro('Something else went wrong'); // does not work either.
dd($attributes); // this works.
abort(422); // this works too.
}
Note: Yes, I know in general one can use middleware or validation services to perform the checks before the request hits the controller, but I don't want to. I need to do it this way.
As of Laravel 5.6 you can now use for example response()->json([1])->send();.
There is no need for it to be the return value of a controller method.
Note that calling send() will not terminate the output. You may want to call exit; manually after send().
You are probably looking for this:
function checkEverythingIsOk() {
if (checkSomething()) {
return Response::json('Something went wrong');
}
if(checkSomethingElse()) {
return Response::someMacro('Something else is wrong')
}
return null; // all is fine
}
And in the controller method:
$response = $this->checkEverythingIsOk();
if($response !== null) { // $response instanceof Response
return $response;
}
It's probably overkill, but I will throw it in anyway. You might want to look into internal requests. Also this is just pseudoish code, I have not actually done this, so take this bit of information with caution.
// build a new request
$returnEarly = Request::create('/returnearly');
// dispatch the new request
app()->handle($newRequest);
// have a route set up to catch those
Route::get('/returnearly', ...);
Now you can have a Controller sitting at the end of that route and interpret the parameters, or you use multiple routes answered by multiple Controllers/Methods ... up to you, but the approach stays the same.
UPDATE
Ok I just tried that myself, creating a new request and dispatching that, it works this way. Problem is, the execution does not stop after the child-request has exited. It goes on in the parent request. Which makes this whole approach kind of useless.
But I was thinking about another way, why not throw an Exception and catch it in an appropriate place to return a specified response?
Turns out, thats already built into Laravel:
// create intended Response
$response = Response::create(''); // or use the response() helper
// throw it, it is a Illuminate\Http\Exception\HttpResponseException
$response->throwResponse();
Now usually an Exception would be logged and you if you are in Debug mode, you would see it on screen etc. etc. But if you take a look into \Illuminate\Foundation\Exceptions\Handler within the render method you can see that it inspects the thrown Exception if it is an instance of HttpResponseException. If it is then the Response will be returned immediately.
To me the most simple and elegant way is:
response()->json($messages_array, $status_code)->throwResponse();
(you don`t need return)
It can be called from a private function or another class...
I use this in a helper class to check for permissions, and if the user doesn`t have it I throw with the above code.