Smyfony projenizde yetki kontrolü yapmak için symfony’nin voters özelliğini kullanabilirsiniz. Bu yazımda symfony voters nedir ve yetki kontrolleri voters ile nasıl yapılırı anlatacağım.

İçindekiler Tablosu

Symfony Voters Nedir?

Symfony Voters, projenizde yetki kontrollerini yönetebilmenizi ve tüm yetki kontrollerini tek bir yere toplamızı sağlayan yetki kontrol mekanizmasıdır.

Örneklemek gerekirse bir makalenin sadece makale yazarı tarafından görüntülenmesine ve düzenlenebilmesine izin vermek istersek bunu controller içerisinde ekleceğimiz bir kontrol ile gerçekleştirebiliriz ya da symfony voters özelliğini kullanabiliriz.

Bu senaryodaki kontrolü controller içinde yapmak isterseniz. Sorunu çabucak çözebilirsiniz. Ya da çözemez misiniz? Cevap: Çözemezsiniz. Çünkü buraya ekleceğiniz kontrolü büyük ihtimalle ara yüzde görüntüleme butonunu gizleme vs. gibi farklı yerlerde uygulamak isteyeceksiniz. Peki bu durumda ne olacak? Her yere aynı kontrolü yazacaksınız. Kod tekrarı yapmak zorunda kalacaksınız ve en küçük bir değişiklikte her yeri güncellemeniz gerekecektir.

Symfony bu sorunu çözmek için voters özelliğini sunuyor. Bu senaryodaki ya da farklı durumlar da ki yetki kontrolleriniz için bir voter tanımlamak ve bunu istediğiniz yerde kullanmak daha kolay ve kullanışlıdır.

Yukarıdaki senaryomuzu kod üzerinden inceleyelim. Controller’da yapılacak olan sahiplik kontrolü aşağıdaki gibi olacaktır. Örneğimizde $post‘un owner bilgisi ile oturumu açık olan kullanıcının bilgisi aynı değilse hata olarak AccessDeniedException fırlatılıyor.

// src/Controller/PostController.php

class PostController extends AbstractController
{
    /**
     * @Route("/posts/{id}", name="post_show")
     */
    public function show($id)
    {
        // Post objesinin çekildiğini varsayalım
        $post = ...;

        // Post objesinin owner bilgisi ile oturumu açık olan kullanıcın bilgileri aynı mı diye kontrol ediliyor.
        if ($post->getOwner() !== $this->getUser()) {
            throw $this->createAccessDeniedException();
        }

        // ...
    }

    /**
     * @Route("/posts/{id}/edit", name="post_edit")
     */
    public function edit($id)
    {
        // Post objesinin çekildiğini varsayalım
        $post = ...;

        // Post objesinin owner bilgisi ile oturumu açık olan kullanıcın bilgileri aynı mı diye kontrol ediliyor.
        if ($post->getOwner() !== $this->getUser()) {
            throw $this->createAccessDeniedException();
        }

        // ...
    }
}

Symfony Voter İle Yetki Kontrolü

Controller’da Yetki Kontrolü

Voter tanımlamış olsaydık ve edit ve view yetkilerimiz olsaydı bu kontroller nasıl olacaktı birde onu inceleyelim. Dikkat ederseniz burada herhangi bir hata fırlatma işlemi de yapmadık. Çünkü bu işlemi voter kendisi yönetiyor.

// src/Controller/PostController.php

class PostController extends AbstractController
{
    /**
     * @Route("/posts/{id}", name="post_show")
     */
    public function show($id)
    {
        // Post objesinin çekildiğini varsayalım
        $post = ...;

        // Voter ile "view" yetkisi kontrolü yapılıyor
        $this->denyAccessUnlessGranted('view', $post);

        // ...
    }

    /**
     * @Route("/posts/{id}/edit", name="post_edit")
     */
    public function edit($id)
    {
        // Post objesinin çekildiğini varsayalım
        $post = ...;

        // Voter ile "edit" yetkisi kontrolü yapılıyor
        $this->denyAccessUnlessGranted('edit', $post);

        // ...
    }
}

Twig’de Yetki Kontrolü

Edit ve view yetkilerinin twig dosyalarımızın için de kontrolünü twig’in is_granted fonksiyonu ile gerçekleştiriyoruz.

// templates/post_list.html.twig

    //... Diğer işlemler

    {% if (is_granted('edit', post) %}
        // Düzenleyebilir
    {% endif %}

    {% if (is_granted('view', post) %}
        // Görüntüleyebilir
    {% endif %}

    //... Diğer işlemler

Symfony Voter Nasıl Tanımlanır?

Voter’larımızı src/Security/ dizini içerisine tanımlıyoruz. Bizim voter’ımızın adı PostVoter olacak. Voter’ımızın boş hali aşağıdaki gibi olacaktır.

// src/Security/PostVoter.php
namespace App\Security;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class PostVoter extends Voter
{
    protected function supports($attribute, $subject): bool
    {
        //
    }

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
    {
       //
    }
}

Burada iki methodumuz mevcut. Bir yetki için kontrol sağladığınızda tüm voter’lardan sonuç alınır. Sadece ilgili voter’ın çalışmasını sağlamak için supports methodu kullanılır. Bu nedenle yetki kontrolü sırasında bu voter’ın çalışıp çalışmayacağına karar veren kontroller buraya tanımlanır. supports methodu true değer return ederse voteOnAttribute methodu çağırılarak yetki için kontroller gerçekleştirilir. Eğer supports methodu false return edersede bu voter çekimser kalmış kabul edilir.

Senaryomuz için supports methodu aşağıdaki gibi olmalı. Bu method içinde gelen yetkinin ‘edit’ ya da ‘view’ olup olmadığı kontrol ediliyor. Aynı zamanda $subject Post objesimi diye kontrol ediliyor. Eğer bu kontrollerden true dönerse voter voteOnAttribute methodunu çağırarak yetki kontrolü yapacaktır. Ancak false döner ise bu voter atlanacak ve diğer voter’lar kontrol edilecektir. Bu durumda voter yetki için çekimser kalmış oluyor.

// src/Security/PostVoter.php
namespace App\Security;

use App\Entity\Post;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class PostVoter extends Voter
{
    protected function supports($attribute, $subject): bool
    {
        // Eğer kontrol edilecek yetki 'view' ya da 'edit' değilse false return et
        if (!in_array($attribute, ['view', 'edit'])) {
            return false;
        }

        // Eğer kontrol edilen $subject Post objesi değilse false return et
        if (!$subject instanceof Post) {
            return false;
        }

        return true;
    }

    protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token)
    {
        //
    }
}

VoteOnAttribute methodunda ihtiyacınız olduğunda TokenInterface üzerinden oturum açmış olan kullanıcının bilgisine ulaşabilirsiniz. Ancak bu durum da tokenInterface’den gerçekten user objesi gelip gelmediğini kontrol etmekte fayda var çünkü oturum açılmamış bir ekranda yetki kontrollü yaptırırsanız burada hata meydana gelecektir.

// src/Security/PostVoter.php
namespace App\Security;

use App\Entity\Post;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class PostVoter extends Voter
{
    protected function supports($attribute, $subject): bool
    {
        // Eğer kontrol edilecek yetki 'view' ya da 'edit' değilse false return et
        if (!in_array($attribute, ['view', 'edit'])) {
            return false;
        }

        // Eğer kontrol edilen $subject Post objesi değilse false return et
        if (!$subject instanceof Post) {
            return false;
        }

        return true;
    }

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        $user = $token->getUser();

        if (!$user instanceof User) {
            return false;
        }
    }
}

Ve geldik sahiplik kontrolüne. Post‘un User‘a ait olup olmadığını checkOwnership methodunu kullanarak gerçekleştireceğiz.

// src/Security/PostVoter.php
namespace App\Security;

use App\Entity\Post;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class PostVoter extends Voter
{
    protected function supports($attribute, $subject)
    {
        if (!in_array($attribute, ['view', 'edit'])) {
            return false;
        }

        if (!$subject instanceof Post) {
            return false;
        }

        return true;
    }

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        $user = $token->getUser();

        if (!$user instanceof User) {
            return false;
        }

        /** @var Post $post */
        $post = $subject;

        switch ($attribute) {
            case 'view':
                return $this->checkOwnership($post, $user);
            case 'edit':
                return $this->checkOwnership($post, $user);
        }

        throw new \LogicException('Beklenmedik hata!');
    }

    private function checkOwnership(Post $post, User $user)
    {
        return $user === $post->getOwner();
    }
}

Voter içinde genel olarak izinlerin kontrolleri yapılıyor. Ama SUPER_ADMIN gibi bir rol tanımladıysanız bu role sahip tüm kullanıcılara izin vermek gerekecektir. Bunun için de voter içinde rol kontrolü ekleyebilirsiniz.

// src/Security/PostVoter.php

// ...
use Symfony\Component\Security\Core\Security;

class PostVoter extends Voter
{
    // ...

    private $security;

    public function __construct(Security $security)
    {
        $this->security = $security;
    }

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        // ...

        // ROLE_SUPER_ADMIN ise izin ver
        if ($this->security->isGranted('SUPER_ADMIN')) {
            return true;
        }

        // ... diğer kontroller
    }
}

Access Decision Strategy Türleri

Symfony voters 4 farklı strajiye bağlı olarak işleme izin verir ya da reddeder. Varsayılan olarak en az bir voter’ın voteOnAttribute methodundan true dönmesi symfony’inin işleme izin vermesi için yeterlidir. Ancak bu durumu değiştirmek isterseniz tanımlı olan 4 strateji şunlardır:

Affirmative (Varsayılan)

En az bir voter’dan true değeri yeterlidir.

Consensus

Voter’lardan dönen cevaplarda true değer sayısının false değer sayısından fazla olması gerekir.

Unanimous

Voter’larda voteOnAttribute methodunda hiç bir false değeri dönmemişse bütün voter’ların çekimser kaldığı durumda allow_if_all_abstain değerine göre işlem yapılır. Bu değer varsayılan olarak false'dur.

Priority

Bu strateji v5.1 ile kullanılmaya başlanmıştır. v5.1 ve üzerinde kullanılabilir. Priority stratejisi kullanıldığında voteOnAttribute methodundan true ya da false return eden ilk voter’ın değeri kullanılır.

Varsayılan stratejiyi ve allow_if_all_abstain değerini değiştirmek için securtiy.yaml dosyasınızda kullanmak istediğiniz değerleri tanımlamanız gerekmektedir.

# config/packages/security.yaml
security:
    access_decision_manager:
        strategy: unanimous
        allow_if_all_abstain: false

Bu yazımda Symfony Voter’ın nasıl kullanıldığını ve ne için kullanılması gerektiğini anlattım. Symfony Voter hakkında daha ayrıntılı bilgi için smyfony voter’ın kendi dökümanını buradan inceleyebilirsiniz.

1 Yorum

CEVAP VER

Lütfen yorumunuzu giriniz!
Lütfen isminizi buraya giriniz