Add propper access control for snips with public option

This commit is contained in:
Tim 2023-04-05 00:02:47 +02:00
parent 607435bff0
commit 693f83ca4a
8 changed files with 117 additions and 4 deletions

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20230404215108 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE snip ADD public TINYINT(1) NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE snip DROP public');
}
}

View File

@ -5,6 +5,7 @@ namespace App\Controller;
use App\Entity\Snip; use App\Entity\Snip;
use App\Form\SnipType; use App\Form\SnipType;
use App\Repository\SnipRepository; use App\Repository\SnipRepository;
use App\Security\Voter\SnipVoter;
use App\Service\SnipServiceFactory; use App\Service\SnipServiceFactory;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
@ -33,6 +34,8 @@ class SnipController extends AbstractController
#[Route('/single/{snip}', name: '_single')] #[Route('/single/{snip}', name: '_single')]
public function single(Snip $snip): Response public function single(Snip $snip): Response
{ {
$this->denyAccessUnlessGranted(SnipVoter::VIEW, $snip);
return $this->render('snip/single.html.twig', [ return $this->render('snip/single.html.twig', [
'snip' => $snip, 'snip' => $snip,
'content' => $this->snipServiceFactory->create($snip)->get(), 'content' => $this->snipServiceFactory->create($snip)->get(),
@ -42,6 +45,8 @@ class SnipController extends AbstractController
#[Route('/raw/{snip}', name: '_raw')] #[Route('/raw/{snip}', name: '_raw')]
public function raw(Snip $snip, Request $request): Response public function raw(Snip $snip, Request $request): Response
{ {
$this->denyAccessUnlessGranted(SnipVoter::VIEW, $snip);
$response = new Response( $response = new Response(
$this->snipServiceFactory->create($snip)->get(), $this->snipServiceFactory->create($snip)->get(),
Response::HTTP_OK, Response::HTTP_OK,
@ -67,6 +72,8 @@ class SnipController extends AbstractController
#[Route('/edit/{snip}', name: '_edit')] #[Route('/edit/{snip}', name: '_edit')]
public function edit(Snip $snip, Request $request): Response public function edit(Snip $snip, Request $request): Response
{ {
$this->denyAccessUnlessGranted(SnipVoter::EDIT, $snip);
$form = $this->createForm(SnipType::class, $snip); $form = $this->createForm(SnipType::class, $snip);
$form->add('Save', SubmitType::class); $form->add('Save', SubmitType::class);
if ($snip->getId()) { if ($snip->getId()) {

View File

@ -19,6 +19,9 @@ class Snip
#[ORM\Column(length: 255)] #[ORM\Column(length: 255)]
private ?string $name = null; private ?string $name = null;
#[ORM\Column]
private ?bool $public = null;
public function __toString(): string public function __toString(): string
{ {
return $this->name ?? ''; return $this->name ?? '';
@ -40,4 +43,16 @@ class Snip
return $this; return $this;
} }
public function isPublic(): ?bool
{
return $this->public;
}
public function setPublic(bool $public): self
{
$this->public = $public;
return $this;
}
} }

View File

@ -18,6 +18,7 @@ class SnipType extends AbstractType
'attr' => ['rows' => 20], 'attr' => ['rows' => 20],
'mapped' => false, 'mapped' => false,
]) ])
->add('public')
; ;
} }

View File

@ -0,0 +1,47 @@
<?php
namespace App\Security\Voter;
use App\Entity\Snip;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;
class SnipVoter extends Voter
{
public const EDIT = 'edit';
public const VIEW = 'view';
protected function supports(string $attribute, mixed $subject): bool
{
// replace with your own logic
// https://symfony.com/doc/current/security/voters.html
return in_array($attribute, [self::EDIT, self::VIEW])
&& $subject instanceof Snip;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
/** @var Snip $subject */
$user = $token->getUser();
// if the user is anonymous, do not grant access
if (!$user instanceof UserInterface) {
return false;
}
// ... (check conditions and return true to grant permission) ...
switch ($attribute) {
case self::VIEW:
if ($subject->isPublic()) {
return true;
}
case self::EDIT:
if ($subject->getCreatedBy() === $user) {
return true;
}
break;
}
return false;
}
}

View File

@ -0,0 +1,9 @@
{% if snip.public %}
<span class="badge bg-info">
<i class="fa fa-lock-open"></i>
</span>
{% else %}
<span class="badge bg-secondary">
<i class="fa fa-lock"></i>
</span>
{% endif %}

View File

@ -6,7 +6,7 @@
<div class="list-group"> <div class="list-group">
{% for snip in snips %} {% for snip in snips %}
<a class="list-group-item" href="{{ path('snip_single', {snip: snip.id}) }}"> <a class="list-group-item" href="{{ path('snip_single', {snip: snip.id}) }}">
{{ snip }} {{ include('snip/badge.html.twig', {snip: snip}) }} {{ snip }}
</a> </a>
{% endfor %} {% endfor %}
</div> </div>

View File

@ -3,9 +3,12 @@
{% block body %} {% block body %}
<div class="card" style="width: 100%;"> <div class="card" style="width: 100%;">
<h4 class="card-header"> <h4 class="card-header">
<a class="btn btn-sm btn-outline-info" href="{{ path('snip_edit', {snip: snip.id}) }}"> {% if is_granted('edit', snip) %}
<i class="fa fa-pencil" aria-hidden="true"></i> <a class="btn btn-sm btn-outline-info" href="{{ path('snip_edit', {snip: snip.id}) }}">
</a> <i class="fa fa-pencil" aria-hidden="true"></i>
</a>
{% endif %}
{{ include('snip/badge.html.twig', {snip: snip}) }}
{{ snip }} {{ snip }}
</h4> </h4>
<div class="card-body"> <div class="card-body">