Erişim Denetimi (Yetkilendirme)

Yetkilendirme, bir kullanıcının örneğin belirli bir kaynağa erişmek ya da bir eylemi gerçekleştirmek için yeterli yetkiye sahip olup olmadığını denetler. Yetkilendirme, önceden başarılı bir kimlik doğrulamayı, yani kullanıcının giriş yapmış olmasını varsayar.

Kurulum ve gereksinimler

Örneklerde Nette\Security\User sınıfının bir nesnesini kullanacağız; bu nesne geçerli kullanıcıyı temsil eder ve onu, bağımlılık enjeksiyonuyla size aktarılmasını sağlayarak elde edersiniz. Presenter'larda yalnızca $user = $this->getUser() çağırın.

Kullanıcı yetkilerinin ayrıştırılmadığı, yönetim bölümü olan çok basit web siteleri için, yetkilendirme ölçütü olarak zaten bildiğimiz isLoggedIn() metodu kullanılabilir. Başka bir deyişle: bir kullanıcı giriş yapar yapmaz tüm yetkilere sahip olur, tersi de geçerlidir.

if ($user->isLoggedIn()) { // kullanıcı giriş yapmış mı?
	deleteItem(); // o zaman işlem için yetkisi var
}

Roller

Rollerin amacı, yetkiler üzerinde daha kesin bir denetim sunmak ve kullanıcı adından bağımsız kalmaktır. Bir kullanıcı giriş yapar yapmaz, ona içinde davranacağı bir ya da birden çok rol atanır. Roller basit dizeler olabilir; örneğin admin, member, guest vb. Bunlar, SimpleIdentity yapıcısının ikinci argümanı olarak, ya bir dize ya da dizelerden oluşan bir dizi olarak, yani roller olarak belirtilir.

Yetkilendirme ölçütü olarak şimdi, kullanıcının verilen rolde davranıp davranmadığını açığa çıkaran isInRole() metodunu kullanacağız:

if ($user->isInRole('admin')) { // kullanıcı admin rolünde mi?
	deleteItem(); // o zaman işlem için yetkisi var
}

Zaten bildiğiniz gibi, kullanıcının çıkışını yapmak onun kimliğini silmek zorunda değildir. Böylece getIdentity() metodu, verilen tüm roller dahil SimpleIdentity nesnesini döndürmeyi sürdürür. Nette Framework, daha az yazmanın daha güvenli koda yol açtığı “daha az kod, daha çok güvenlik” ilkesini benimser. Bu yüzden rolleri denetlerken kullanıcının giriş yapmış olup olmadığını doğrulamanız gerekmez. isInRole() metodu etkin rollerle çalışır: kullanıcı giriş yapmışsa kimlikte belirtilen rolleri temel alır; giriş yapmamışsa otomatik olarak özel guest rolüne sahip olur (ya da bir tane sağlanmışsa konuk kimliğinin rollerine).

Yetkilendirici

Rollerin yanı sıra kaynak ve işlem terimlerini de tanıtacağız:

  • rol, kullanıcının bir özelliğidir; örneğin moderatör, editör, ziyaretçi, kayıtlı kullanıcı, yönetici…
  • kaynak, web sitesinin mantıksal bir birimidir; makale, sayfa, kullanıcı, menü öğesi, anket, presenter, …
  • işlem, kullanıcının kaynakla yapabileceği ya da yapamayacağı belirli bir etkinliktir; örneğin görüntüleme, düzenleme, silme, oy verme, …

Yetkilendirici, verilen bir rolün, belirli bir kaynakla belirli bir işlemi gerçekleştirme yetkisine sahip olup olmadığına karar veren nesnedir. Tek bir isAllowed() metoduyla Nette\Security\Authorizator arayüzünü gerçekleştiren bir nesnedir:

class MyAuthorizator implements Nette\Security\Authorizator
{
	public function isAllowed($role, $resource, $operation): bool
	{
		if ($role === 'admin') {
			return true;
		}
		if ($role === 'user' && $resource === 'article') {
			return true;
		}

		// ...

		return false;
	}
}

Yetkilendiriciyi yapılandırmaya DI container'ın bir servisi olarak ekleriz:

services:
	- MyAuthorizator

Ve işte bir kullanım örneği. Dikkat edin, bu kez yetkilendiricininkini değil Nette\Security\User::isAllowed() metodunu çağırıyoruz, dolayısıyla ilk $role parametresi yok. Bu metot, kullanıcının tüm rolleri için sırayla MyAuthorizator::isAllowed() çağırır ve en az biri yetkiliyse true döndürür.

if ($user->isAllowed('file')) { // kullanıcı 'file' kaynağıyla herhangi bir şey yapabilir mi?
	useFile();
}

if ($user->isAllowed('file', 'delete')) { // kullanıcı 'file' kaynağı üzerinde 'delete' yapabilir mi?
	deleteFile();
}

İki argüman da isteğe bağlıdır; varsayılan null değeri her şey anlamına gelir.

Permission ACL

Nette, yetkilendiricinin yerleşik bir gerçekleştirimiyle gelir: programcıya yetkileri ve erişimi yönetmek için hafif ve esnek bir ACL (Access Control List) katmanı sunan Nette\Security\Permission sınıfı. Onunla çalışmak; rolleri, kaynakları ve tek tek yetkileri tanımlamaktan oluşur. Roller ve kaynaklar hiyerarşi oluşturmaya izin verir. Açıklamak için bir web uygulaması örneği göstereceğiz:

  • guest: web sitesinin herkese açık bölümünü okuyup gezebilen, yani makaleleri ve yorumları okuyabilen, anketlerde oy verebilen kayıtsız ziyaretçi.
  • registered: giriş yapmış, ayrıca yorum da yazabilen kayıtlı kullanıcı.
  • admin: makaleleri, yorumları ve anketleri yönetebilir.

Belirli roller (guest, registered ve admin) tanımladık ve belirli bir role sahip kullanıcıların erişebileceği ya da üzerinde belirli işlemleri (view, vote, add, edit) gerçekleştirebileceği kaynakları (article, comment, poll) andık.

Permission sınıfının bir örneğini oluşturur ve rolleri tanımlarız. Rol kalıtımı denen şeyi kullanmak mümkündür; bu, örneğin admin rolündeki bir kullanıcının sıradan bir web sitesi ziyaretçisinin yapabildiklerini de (elbette daha fazlasını da) yapabilmesini sağlar.

$acl = new Nette\Security\Permission;

$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // 'registered', 'guest'ten kalıtır
$acl->addRole('admin', 'registered'); // 'admin', 'registered'dan kalıtır

Şimdi kullanıcıların erişebileceği kaynakların listesini tanımlarız.

$acl->addResource('article');
$acl->addResource('comment');
$acl->addResource('poll');

Kaynaklar da kalıtımı kullanabilir; örneğin $acl->addResource('perex', 'article') girmek mümkün olurdu.

Ve şimdi en önemli bölüm. Aralarında, kimin neyle ne yapabileceğini belirleyen kuralları tanımlarız:

// başlangıçta kimse hiçbir şey yapamaz

// guest makaleleri, yorumları ve anketleri görüntüleyebilsin
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// ve anketlerde oy verebilsin
$acl->allow('guest', 'poll', 'vote');

// registered, yetkileri guest'ten kalıtır; ona ek olarak yorum yapma hakkı verelim
$acl->allow('registered', 'comment', 'add');

// yönetici her şeyi görüntüleyip düzenleyebilir
$acl->allow('admin', $acl::All, ['view', 'edit', 'add']);

Peki birinin belirli bir kaynağa erişmesini engellemek istersek?

// yönetici anketleri düzenleyemez; bu demokratik olmazdı
$acl->deny('admin', 'poll', 'edit');

Kural kümesini oluşturduğumuza göre, yetkilendirme sorgularını kolayca yöneltebiliriz:

// guest makaleleri görüntüleyebilir mi?
$acl->isAllowed('guest', 'article', 'view'); // true

// guest makaleleri düzenleyebilir mi?
$acl->isAllowed('guest', 'article', 'edit'); // false

// guest anketlerde oy verebilir mi?
$acl->isAllowed('guest', 'poll', 'vote'); // true

// guest yorum yapabilir mi?
$acl->isAllowed('guest', 'comment', 'add'); // false

Aynısı kayıtlı bir kullanıcı için de geçerlidir, ama o ayrıca yorum da yapabilir:

$acl->isAllowed('registered', 'article', 'view'); // true
$acl->isAllowed('registered', 'comment', 'add'); // true
$acl->isAllowed('registered', 'comment', 'edit'); // false

Yönetici, anketler dışında her şeyi düzenleyebilir:

$acl->isAllowed('admin', 'poll', 'vote'); // true
$acl->isAllowed('admin', 'poll', 'edit'); // false
$acl->isAllowed('admin', 'comment', 'edit'); // true

Yetkiler dinamik olarak da değerlendirilebilir; kararı, tüm parametrelerin aktarıldığı kendi callback'imize bırakabiliriz:

$assertion = function (Permission $acl, ?string $role, ?string $resource, ?string $privilege): bool {
	return /* ... */;
};

$acl->allow('registered', 'comment', null, $assertion);

Peki yalnızca rol ve kaynak adlarının yetmediği, örneğin registered rolünün article kaynağını yalnızca onun yazarıysa düzenleyebileceğini tanımlamak istediğimiz bir durumu nasıl ele alırız? Dizeler yerine nesneler kullanacağız; rol bir Nette\Security\Role nesnesi, kaynak ise bir Nette\Security\Resource nesnesi olacak. Onların getRoleId() ve getResourceId() metotları özgün dizeleri döndürecek:

class Registered implements Nette\Security\Role
{
	public $id;

	public function getRoleId(): string
	{
		return 'registered';
	}
}


class Article implements Nette\Security\Resource
{
	public $authorId;

	public function getResourceId(): string
	{
		return 'article';
	}
}

Ve şimdi kuralı oluşturuyoruz:

$assertion = function (Permission $acl, ?string $role, ?string $resource, ?string $privilege): bool {
	$role = $acl->getQueriedRole(); // Registered nesnesi
	$resource = $acl->getQueriedResource(); // Article nesnesi
	return $role->id === $resource->authorId;
};

$acl->allow('registered', 'article', 'edit', $assertion);

ACL sorgusu ise nesneler aktarılarak yapılır:

$user = new Registered(/* ... */);
$article = new Article(/* ... */);
$acl->isAllowed($user, $article, 'edit');

Bir rol tek bir rolden ya da birden çok rolden kalıtabilir. Peki atalardan birinde eylem yasaklı, diğerinde izinliyse ne olur? Alt rolün hakları ne olacak? Bunu rolün ağırlığı belirler: atalar listesinde en son anılan rolün ağırlığı en yüksek, ilkinin en düşüktür. Bu, örnekten daha iyi anlaşılır:

$acl = new Nette\Security\Permission;
$acl->addRole('admin');
$acl->addRole('guest');

$acl->addResource('backend');

$acl->allow('admin', 'backend');
$acl->deny('guest', 'backend');

// durum A: admin rolünün ağırlığı guest rolünden düşük
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false

// durum B: admin rolünün ağırlığı guest rolünden yüksek
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true

Roller ve kaynaklar kaldırılabilir de (removeRole(), removeResource()), kurallar da geri alınabilir (removeAllow(), removeDeny()). Tüm doğrudan ana rollerin dizisini getRoleParents() döndürür. İki varlığın birbirinden kalıtıp kalıtmadığını roleInheritsFrom() ve resourceInheritsFrom() döndürür. Bir rolün ya da kaynağın varlığı hasRole() ve hasResource() ile denetlenir, kayıtlı tüm rollerin ve kaynakların listelerini getRoles() ve getResources() döndürür, her şey ise removeAllRoles() ve removeAllResources() ile silinebilir.

Servis Olarak Ekleme

Oluşturduğumuz ACL'yi, $user nesnesinin onu kullanmaya başlaması için, yani kodda $user->isAllowed('article', 'view') kullanmanın mümkün olması için yapılandırmaya bir servis olarak aktarmamız gerekir. Bunun için ona bir factory yazacağız:

namespace App\Model;

class AuthorizatorFactory
{
	public static function create(): Nette\Security\Permission
	{
		$acl = new Nette\Security\Permission;
		$acl->addRole(/* ... */);
		$acl->addResource(/* ... */);
		$acl->allow(/* ... */);
		return $acl;
	}
}

Ve onu yapılandırmaya ekleyeceğiz:

services:
	- App\Model\AuthorizatorFactory::create

Presenter'larda yetkileri sonra örneğin startup() metodunda doğrulayabilirsiniz:

protected function startup()
{
	parent::startup();
	if (!$this->getUser()->isAllowed('backend')) {
		$this->error('Forbidden', 403);
	}
}
versiyon: 4.x