Laravel Policies - How to Pass Multiple Arguments to function - php

I'm trying to authorize a users character to delete/update post. I was using policies to do so, but I could only pass one parameter to the policy function. If I pass more than the user and another variable, the variable isn't passed into the function.
Models: User has many characters, a character can post multiple posts. So for authorization purposes, I would have to compare the post's character_id with the current character's id...-
Per the docs, you can pass more multiples to the Gate Facade:
Gate::define('delete-comment', function ($user, $post, $comment) {
//
});
But I couldn't find anyway to do so with policies. What I had to do was to inject the Request object to get the object needed for authorization. Basically I wouldn't even need the User Object.
public function update(User $user, Post $post)
{
return $user->id === $post->user_id;
}
Using the Request object works, but it feels very hacky. Is there a nicer way to achieve this?
edit:
In the CharacterLocationController I have a method show and I want to authorize the action before showing the resource.
public function show(Request $request, Character $character, Location $location)
{
$this->authorize([$location, $character]);
...
}
The policy is registered like this: 'App\Location' => 'App\Policies\LocationPolicy' in the AuthServiceProvider
I dumped the array passed to the policy function, and it only outputs the $location.
public function show(User $user, $data) {
dd($data); // expecting location and character
return !$location->private || $location->authorized->contains($this->character);
}

I think there is possibly some confusion here on what functions are doing what.
When you use
Gate::define('delete-comment', function ($user, $post, $comment) {
//
});
Or in the CommentPolicy
public function delete(User $user, Post $post, Comment $comment)
{
return $user->id === $post->user_id;
}
All you are doing is defining the rules. At this point, we aren't worried about passing anything, only that the objects we received can or should be able to interact with each other. The only difference between these two is when using policies, it's just an easy way to abstract all your rules into one simple and easy to read class. If you have an app with potentially hundreds of tables and models, it will get confusing fast if you have these rules littered all over your app so policies would help to keep them all organized.
It's when you are actually checking if someone has permission to do something when you should be passing these items along. For example, when you do the following,
if (Gate::allows('delete-comment', [$post, $comment])) {
//
}
Or if in the CommentController
$this->authorize('delete', [$post, $comment]);
That is what controls which parameters are going to be passed to the policy or the Gate::define method. According to the docs, the $user parameter is already added for you so in this case, you only need to worry about passing the correct $post and $comment being modified.

Related

Laravel cache with route model binding?

I'm adding caching to my Laravel app routes. I have a function that renders a blog post on my site:
public function show(Post $post)
{
SEO::setTitle($post->title);
SEO::setDescription($post->subtitle);
SEO::setCanonical('https://employbl.com/blog/' . $post->slug);
SEO::opengraph()->setUrl('https://employbl.com/blog/' . $post->slug);
SEO::opengraph()->addProperty('type', 'article');
SEO::opengraph()->addImage($post->featured_image);
SEO::twitter()->setSite('#Employbl_Jobs');
$markdown = Markdown::parse($post->body);
return view('blog.post', compact('post', 'markdown'));
}
This is the route that calls the method: Route::get('/blog/{post}', 'PostController#show')->name('posts.show'); so that my blog renders a URL with a slug like: https://employbl.com/blog/laravel-vue-tailwindcss-single-page-application-spa
What is the best way to implement caching on this route so the page loads faster for users?
Would it be something like:
$post = Cache::rememberForever('blog-post' . $post->id, function(){
return $post;
});
Or is caching even necessary with route model binding? Does the cache key need to be unique or can I just use "blog-post" as cache key? Would it be better to cache the $markdown variable instead of the $post variable? Both?
You've got a few questions in here, so I'll try to answer each. The answers may not be letter perfect as I am going from memory without any way to reference or confirm them myself at the moment.
If you're trying to cache the final output of your view, you can effectively do it be replacing your final view call with:
return Cache::rememberForever('blog-post' . $post->id, function() use ($post) {
// Do your SEO and markdown stuff here
return view('blog.post', compact('post', 'markdown'))->render();
});
The cache key needs to be unique for the post. The model routing system knows nothing about the cache system, it's just a way of passing a value to a controller which makes some assumptions about the incoming data based on the URI. So what you are doing currently is fine.
The problem your question about should I cache the post, the markdown or both? is that it probably won't make a difference
1) You're calling a model GET route. This has the effect of loading the Post from the DB each time, making the caching of the Post itself irrelevant. This is true even with the caching of the render view itself.
2) Your view call requires the Post itself as a parameter [of compact()]. You'll need to load it from somewhere, so that means a database call again to retrieve the post.
3) You're using Cache::rememberForever which means the cache will never expire. So loading the Post after the first time will be pointless, since it will never be used again (the results are cached forever!). Future edits (if any) won't work unless you invalidate the cache (which makes rememberForever kind of pointless).
So, I recommend, for this case, that you move away from the model route and instead try a traditional id based Route
public function show(Request $request, $id)
{
return Cache::remember('blog-post'.$id, ttl, function() use($id) {
$post = Post::find($id);
// Do SEO and markdown stuff
return view('blog.post', compact('post', 'markdown'))->render();
});
}
where ttl is the time for the cache to expire.
I was looking to solve a similar issue with caching models that were bound using Route Model Binding and found the following solution.
// On the Model class add the following method.
public function resolveRouteBinding($value, $field = null): ?Model
{
return Cache::remember('my.custom.key'.$value, 3600, function () use ($value) {
return $this->where('slug', $value)->firstOrFail();
});
}
The method details can be found here: Customizing Resolution Logic
It's worth noting that there's a very possible chance that you'd rather use this without the Cache::remember() method so that you're not caching something that returns null. It may be better to do this in the following way instead:
// On the Model class add the following method.
public function resolveRouteBinding($value, $field = null): ?Model
{
$cacheName = "my.custom.key.{$value}";
if (Cache::has($cacheName)) {
return Cache::get($cacheName);
}
$result = $this->query('slug', $value)->firstOrFail();
Cache::put($cacheName, $result, 3600);
return $result;
}

How can we return reason for action denial from inside Laravel policy?

Let's say we have action in the policy for our model that can return false in bunch of different scenarios:
class PostPolicy
{
public function publish(User $user, Post $post)
{
if ($post->user_id !== $user->id) {
return false;
}
return $post->show_at->lessThan(now());
}
}
As you can see, we are denying this user his publishing rights in two cases: if it's not his post or if this post was prepared in advance for some future date that is not yet due.
How can I provide some context as to why authorization failed? Was it because we are not the owner or was it because it's not time yet for this post to be published?
$user->can('publish', $post); // if this returns false we don't know
// what caused authorization to fail.
It looks that Laravel policies by design doesn't have any way of doing that. But I am curious as to what workarounds there can possibly be so that we can have authorization logic (no matter how intricate) in one place (model's policy) and also get some context (i.e., custom error codes) when authorization fails.
Any ideas?
In case any one needed,
apart from above accepted answer, in Laravel 7+, Gate can provide reasons for denial,
Reference: https://laravel.com/docs/7.x/authorization#gate-responses
Gate::authorize() calls will throw reason along with the exception with message provided in the Response::deny(<message>) call, the exception itself will be an Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
$user->can() or Gate::allows() will give boolean
Gate::inspect() will give full response
Given that you will now return a Gate Response object instead of boolean, Laravel will help on returning suitable response stated above.
<?php
use Illuminate\Auth\Access\Response;
Originally, you only return a boolean
<?php
class PostPolicy
{
public function publish(User $user, Post $post)
{
return $post->user_id !== $user->id;
}
}
By using Gate Response, you can now provide a reason
<?php
use Illuminate\Auth\Access\Response;
class PostPolicy
{
public function publish(User $user, Post $post)
{
return $post->user_id === $user->id
? Response::allow()
: Response::deny('You are not the author of the post.');
}
}
If in controller you are using the policy like :
<?php
$this->authorize('publish', Post::class)
Then laravel is going to have 403 HTTPResponse error.
What you should do is, one policy method should just be checking one validation case.
For example update your policy :
<?php
// Check if post he is trying to publish is his own
public function publishOwnPost(){...}
// Check if post is for future purpose
public function publicFuturePost(){...}
Then in controller do :
<?php
if(!$user->can('publishOwnPost', $post)){
// Return custom error view for case 1
return response()->view('errors.publishOwnPostError', $data, 403);
}
if(!$user->can('publishFuturePost', $post)){
// Return custom error view for case 2
return response()->view('errors.publishFuturePostError', $data, 403);
}
// Do further processing
What I ended up doing was splitting some of the responsibilities between model and it's policy.
Policy ended up responsible for making sure user has the right to do a specific action. Nothing more or less:
class PostPolicy
{
public function publish(User $user, Post $post)
{
return $post->user_id !== $user->id;
}
}
Model on the other hand must have logic to check whether or not certain action can be performed with it:
class Post extends Model
{
...
public function isPublishable()
{
return $this->show_at->lessThan(now());
}
...
}
Therefore each post instance now can tell us whether or not it can be published. Lastly my Post::publishBy(User $user) action will include authorizing user for this action first and checking if this post can be published separately so that we can determine specific reason as to why publishing failed.
I feel like this design suits better, leaving Laravel policies to do only what they are supposed to be doing (authorizing user actions) and requiring models to be responsible for things that only concern them.

Symfony 4 Voter Annotations (#IsGranted)

I'm trying to use Symfony Voters and Controller Annotation to allow or restrict access to certain actions in my Symfony 4 Application.
As an example, My front-end provides the ability to delete a "Post", but only if the user has the "DELETE_POST" attribute set for that post.
The front end sends an HTTP "DELETE" action to my symfony endpoint, passing the id of the post in the URL (i.e. /api/post/delete/19).
I'm trying to use the #IsGranted Annotation, as described here.
Here's my symfony endpoint:
/**
* #Route("/delete/{id}")
* #Method("DELETE")
* #IsGranted("DELETE_POST", subject="post")
*/
public function deletePost($post) {
... some logic to delete post
return new Response("Deleting " . $post->getId());
}
Here's my Voter:
class PostVoter extends Voter {
private $attributes = array(
"VIEW_POST", "EDIT_POST", "DELETE_POST", "CREATE_POST"
);
protected function supports($attribute, $subject) {
return in_array($attribute, $this->attributes, true) && $subject instanceof Post;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token) {
... logic to figure out if user has permissions.
return $check;
}
}
The problem I'm having is that my front end is simply sending the resource ID to my endpoint. Symfony is then resolving the #IsGranted Annotation by calling the Voters and passing in the attribute "DELETE_POST" and the post id.
The problem is, $post is just a post id, not an actual Post object. So when the Voter gets to $subject instanceof Post it returns false.
I've tried injecting Post into my controller method by changing the method signature to public function deletePost(Post $post). Of course this does not work, because javascript is sending an id in the URL, not a Post object.
(BTW: I know this type of injection should work with Doctrine, but I am not using Doctrine).
My question is how do I get #IsGranted to understand that "post" should be a post object? Is there a way to tell it to look up Post from the id passed in and evaluated based on that? Or even defer to another controller method to determine what subject="post" should represent?
Thanks.
UPDATE
Thanks to #NicolasB, I've added a ParamConverter:
class PostConverter implements ParamConverterInterface {
private $dao;
public function __construct(MySqlPostDAO $dao) {
$this->dao = $dao;
}
public function apply(Request $request, ParamConverter $configuration) {
$name = $configuration->getName();
$object = $this->dao->getById($request->get("id"));
if (!$object) {
throw new NotFoundHttpException("Post not found!");
}
$request->attributes->set($name, $object);
return true;
}
public function supports(ParamConverter $configuration) {
if ($configuration->getClass() === "App\\Model\\Objects\\Post") {
return true;
}
return false;
}
}
This appears to be working as expected. I didn't even have to use the #ParamConverter annotation to make it work. The only other change I had to make to the controller was changing the method signature of my route to public function deletePost(Post $post) (as I had tried previously - but now works due to my PostConverter).
My final two questions would be:
What exactly should I check for in the supports() method? I'm currently just checking that the class matches. Should I also be checking that $configuration->getName() == "id", to ensure I'm working with the correct field?
How might I go about making it more generic? Am I correct in assuming that anytime you inject an entity in a controller method, Symfony will call the supports method on everything that implements ParamConverterInterface?
Thanks.
What would happen if you used Doctrine is that you'd need to type-hint your $post variable. After you've done that, Doctrine's ParamConverter would take care of the rest. Right now, Symfony has no idea how about how to related your id url placeholder to your $post parameter, because it doesn't know which Entity $post refers to. By type-hinting it with something like public function deletePost(Post $post) and using a ParamConverter, Symfony would know that $post refers to the Post entity with the id from the url's id placeholder.
From the doc:
Normally, you'd expect a $id argument to show(). Instead, by creating a new argument ($post) and type-hinting it with the Post class (which is a Doctrine entity), the ParamConverter automatically queries for an object whose $id property matches the {id} value. It will also show a 404 page if no Post can be found.
The Voter would then also know what $post is and how to treat it.
Now since you are not using Doctrine, you don't have a ParamConverter by default, and as we just saw, this is the crucial element here. So what you're going to have to do is simply to define your own ParamConverter.
This page of the Symfony documentation will tell you more about how to do that, especially the last section "Creating a Converter". You will have to tell it how to convert the string "id" into a Post object using your model's logic. At first, you can make it very specific to Post objects (and you may want to refer to that one ParamConverter explicitly in the annotation using the converter="name" option). Later on once you've got a working version, you can make it work more generic.

Policies with extra parameters

I know, how to use Laravel policies and everything works, but I am stuck with create(...) method.
My app is a training diary for ski racers. Each racer can manage (view, add, edit..) his own diary. Each trainer can manage his own diary, diary of all racers but not other trainers. I use native Laravel Policies and it works great during *update(...) and delete(...) method, but not create(...).
TrainingRecordPolicy.php:
public function update(User $user, TrainingRecord $record)
{
if ($user->id == $record->user->id) {
return true;
}
if ($user->isTrainer() && $record->user->isRacer()) {
return true;
}
return false;
}
I'm using it in controllers like $this->authorize('update', $record). But if I want to check, if user can create new record into others user diary, I have no idea how to deal with it.
This doesn't work $this->authorize('create', TrainingRecord::class, $diaryOwner) and if I use $this->authorize('createDiaryRecord', $diaryOwner) it calls method inside UserPolicy.
How do I send $diaryOwner as extra parameter to create() method within the policy?
Note: $diaryOwner is retrieved from the route parameter user in route with signature /diary/{user}
You can access the $diaryOwner within the policy using request() helper.
public function create(User $user)
{
$diaryOwner = request()->user; // because route is defined as /diary/{user}
}
There may be a problem because:
When using dynamic properties, Laravel will first look for the parameter's value in the request payload. If it is not present, Laravel will search for the field in the route parameters.
So instead of using request()->user use request()->route()->parameter('user').
Also if you are using \Illuminate\Routing\Middleware\SubstituteBindings::class you will get User instance.

Using Laravel 5 Method Injection with other Parameters

So I'm working on an admin interface. I have a route set up like so:
Route::controllers([
'admin' => 'AdminController',
]);
Then I have a controller with some methods:
public function getEditUser($user_id = null)
{
// Get user from database and return view
}
public function postEditUser($user_id = 0, EditUserRequest $request)
{
// Process any changes made
}
As you can see, I'm using method injection to validate the user input, so URL's would look like this:
http://example.com/admin/edit-user/8697
A GET request would go to the GET method and a POST request to the POST method. The problem is, if I'm creating a new user, there won't be an ID:
http://examplecom/admin/edit-user/
Then I get an error (paraphrased):
Argument 2 passed to controller must be an instance of EditUserRequest, none given
So right now I'm passing an ID of 0 in to make it work for creating new users, but this app is just getting started, so am I going to have to do this throughout the entire application? Is there a better way to pass in a validation method, and optionally, parameters? Any wisdom will be appreciated.
You can reverse the order of your parameters so the optional one is a the end:
public function postEditUser(EditUserRequest $request, $user_id = null)
{
}
Laravel will then resolve the EditUserRequest first and pass nothing more if there's no user_id so the default value will kick in.

Categories