Reputation: 805
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.
Upvotes: 1
Views: 2398
Reputation: 1071
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.
Upvotes: 2