Skip to content

Commit ebca80b

Browse files
committed
feature #442 Add a security Post Voter (yceruto, javiereguiluz)
This PR was merged into the master branch. Discussion ---------- Add a security Post Voter Implement #440 feature. - [x] Add more code comments and doc references Following some references and recommendations in: - https://symfony.com/doc/current/components/security/authorization.html - http://symfony.com/doc/current/security/voters.html - https://stovepipe.systems/post/symfony-security-roles-vs-voters (@iltar blog) Any suggestion is welcome! Commits ------- a592eea Minor reword in the help note 8468423 Simplified the voter code a bit c14e9db Add security post voter
2 parents 12ff0ab + a592eea commit ebca80b

File tree

4 files changed

+75
-12
lines changed

4 files changed

+75
-12
lines changed

app/Resources/views/blog/post_show.html.twig

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
{% endblock %}
5656

5757
{% block sidebar %}
58-
{% if post.isAuthor(app.user) %}
58+
{% if is_granted('edit', post) %}
5959
<div class="section">
6060
<a class="btn btn-lg btn-block btn-success" href="{{ path('admin_post_edit', { id: post.id }) }}">
6161
<i class="fa fa-edit" aria-hidden="true"></i> {{ 'action.edit_post'|trans }}

app/config/services.yml

+8
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ services:
4848
tags:
4949
- { name: kernel.event_subscriber }
5050

51+
# To inject the voter into the security layer, you must declare it as a service and tag it with security.voter.
52+
# See http://symfony.com/doc/current/security/voters.html#configuring-the-voter
53+
app.post_voter:
54+
class: AppBundle\Security\PostVoter
55+
public: false
56+
tags:
57+
- { name: security.voter }
58+
5159
# Uncomment the following lines to define a service for the Post Doctrine repository.
5260
# It's not mandatory to create these services, but if you use repositories a lot,
5361
# these services simplify your code:

src/AppBundle/Controller/Admin/BlogController.php

+5-11
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,9 @@ public function newAction(Request $request)
119119
*/
120120
public function showAction(Post $post)
121121
{
122-
// This security check can also be performed:
123-
// 1. Using an annotation: @Security("post.isAuthor(user)")
124-
// 2. Using a "voter" (see http://symfony.com/doc/current/cookbook/security/voters_data_permission.html)
125-
if (!$post->isAuthor($this->getUser())) {
126-
throw $this->createAccessDeniedException('Posts can only be shown to their authors.');
127-
}
122+
// This security check can also be performed
123+
// using an annotation: @Security("is_granted('show', post)")
124+
$this->denyAccessUnlessGranted('show', $post, 'Posts can only be shown to their authors.');
128125

129126
return $this->render('admin/blog/show.html.twig', [
130127
'post' => $post,
@@ -139,9 +136,7 @@ public function showAction(Post $post)
139136
*/
140137
public function editAction(Post $post, Request $request)
141138
{
142-
if (!$post->isAuthor($this->getUser())) {
143-
throw $this->createAccessDeniedException('Posts can only be edited by their authors.');
144-
}
139+
$this->denyAccessUnlessGranted('edit', $post, 'Posts can only be edited by their authors.');
145140

146141
$entityManager = $this->getDoctrine()->getManager();
147142

@@ -169,11 +164,10 @@ public function editAction(Post $post, Request $request)
169164
*
170165
* @Route("/{id}/delete", name="admin_post_delete")
171166
* @Method("POST")
172-
* @Security("post.isAuthor(user)")
167+
* @Security("is_granted('delete', post)")
173168
*
174169
* The Security annotation value is an expression (if it evaluates to false,
175170
* the authorization mechanism will prevent the user accessing this resource).
176-
* The isAuthor() method is defined in the AppBundle\Entity\Post entity.
177171
*/
178172
public function deleteAction(Request $request, Post $post)
179173
{

src/AppBundle/Security/PostVoter.php

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace AppBundle\Security;
13+
14+
use AppBundle\Entity\Post;
15+
use AppBundle\Entity\User;
16+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
17+
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
18+
19+
/**
20+
* It grants or denies permissions for actions related to blog posts (such as
21+
* showing, editing and deleting posts).
22+
*
23+
* See http://symfony.com/doc/current/security/voters.html
24+
*
25+
* @author Yonel Ceruto <[email protected]>
26+
*/
27+
class PostVoter extends Voter
28+
{
29+
// Defining these constants is overkill for this simple application, but for real
30+
// applications, it's a recommended practice to avoid relying on "magic strings"
31+
const SHOW = 'show';
32+
const EDIT = 'edit';
33+
const DELETE = 'delete';
34+
35+
/**
36+
* {@inheritdoc}
37+
*/
38+
protected function supports($attribute, $subject)
39+
{
40+
// this voter is only executed for three specific permissions on Post objects
41+
return $subject instanceof Post && in_array($attribute, [self::SHOW, self::EDIT, self::DELETE]);
42+
}
43+
44+
/**
45+
* {@inheritdoc}
46+
*/
47+
protected function voteOnAttribute($attribute, $post, TokenInterface $token)
48+
{
49+
$user = $token->getUser();
50+
51+
// the user must be logged in; if not, deny permission
52+
if (!$user instanceof User) {
53+
return false;
54+
}
55+
56+
// the logic of this voter is pretty simple: if the logged user is the
57+
// author of the given blog post, grant permission; otherwise, deny it.
58+
// (the supports() method guarantees that $post is a Post object)
59+
return $user === $post->getAuthor();
60+
}
61+
}

0 commit comments

Comments
 (0)