アクセス制御(認可)

認可は、利用者が十分な権限を持っているか、たとえば特定の資源に触れられるか、ある操作を行えるかを確かめます。認可は、その前に認証が成功していること、つまり利用者がログインしていることを前提にします。

インストールと要件

例では Nette\Security\Userクラスのオブジェクトを使います。これは今の利用者を表し、dependency injectionで渡してもらえます。プレゼンターでは $user = $this->getUser() を呼ぶだけです。

管理画面のある、利用者の権限を分けないごく単純なウェブサイトなら、もう知っている isLoggedIn() メソッドを認可の基準に使えます。言い換えれば、利用者がログインしていればすべての権限を持ち、その逆も同じ、ということです。

if ($user->isLoggedIn()) { // 利用者はログインしていますか
	deleteItem(); // ならその操作の権限があります
}

ロール

ロールの目的は、権限をより細かく制御しつつ、ユーザー名からは独立していることです。利用者がログインすると、そこで振る舞うロールがひとつ以上割り当てられます。ロールは単純な文字列でよく、たとえば adminmemberguest などです。これは SimpleIdentity のコンストラクタの第 2 引数として、文字列か文字列(ロール)の配列で指定します。

認可の基準としては、利用者がそのロールで振る舞っているかを教えてくれる isInRole() メソッドを使います。

if ($user->isInRole('admin')) { // 利用者は admin のロールですか
	deleteItem(); // ならその操作の権限があります
}

もうご存じのとおり、利用者をログアウトさせても身元は消えるとは限りません。ですから getIdentity() メソッドは、与えられたすべてのロールも含めて SimpleIdentity オブジェクトを返し続けます。Nette Framework は「コードが少ないほど安全」という考え方を掲げていて、書く量が少ないほど安全なコードになります。ですからロールを確かめるとき、利用者がログインしているかを確かめる必要はありません。isInRole() メソッドは実効のロールを扱います。利用者がログインしていれば身元に書かれたロールをもとにし、ログインしていなければ自動的に特別なロール guest(あるいは用意されていれば訪問者の身元のロール)を持ちます。

認可器

ロールのほかに、資源と操作という語も持ち込みます。

  • ロール は利用者の性質です。たとえば管理人、編集者、訪問者、登録した利用者、管理者など
  • 資源 はウェブサイトの論理的なひとまとまりです。記事、ページ、利用者、メニューの項目、アンケート、プレゼンターなど
  • 操作 は利用者が資源に対して行える、あるいは行えない具体的な活動です。たとえば表示、編集、削除、投票など

認可器は、あるひとつの ロール が、特定の 資源 に対してある 操作 を行う権限を持つかを決めるオブジェクトです。これは Nette\Security\Authorizatorインターフェースを、isAllowed() というひとつのメソッドとともに実装するオブジェクトです。

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;
	}
}

この認可器を設定に、DI コンテナのサービスとして足します。

services:
	- MyAuthorizator

そしてこれが使い方の例です。今回は認可器のではなく Nette\Security\User::isAllowed() メソッドを呼んでいるので、第 1 パラメータの $role がないことに注意してください。このメソッドは利用者のすべてのロールについて MyAuthorizator::isAllowed() を順に呼び、そのうち少なくともひとつに権限があれば true を返します。

if ($user->isAllowed('file')) { // 利用者は 'file' の資源に対して何かできますか
	useFile();
}

if ($user->isAllowed('file', 'delete')) { // 利用者は 'file' の資源に対して 'delete' を行えますか
	deleteFile();
}

どちらの引数も省略でき、既定値の null何でも を意味します。

Permission の ACL

Nette には認可器の組み込みの実装、Nette\Security\Permissionクラスがあり、権限とアクセスを管理する軽くしなやかな ACL(Access Control List)の層をプログラマーに与えます。それを扱う作業は、ロール、資源、そして個々の権限を定めることから成ります。ロールと資源は階層を作れます。説明のために、ウェブアプリケーションの例をお見せします。

  • guest: 登録していない訪問者。ウェブサイトの公開の部分を読んで見て回れます。つまり記事とコメントを読み、アンケートに投票できます。
  • registered: ログインした、登録済みの利用者。コメントも投稿できます。
  • admin: 記事、コメント、アンケートを管理できます。

いくつかのロール(guestregisteredadmin)を定め、資源(articlecommentpoll)にも触れました。あるロールを持つ利用者は、それらに触れたり、ある操作(viewvoteaddedit)を行ったりできます。

Permission クラスのインスタンスを作り、ロール を定めます。いわゆるロールの継承も使えて、たとえば admin のロールを持つ利用者が、ふつうのウェブサイトの訪問者にできることも(もちろんそれ以上のことも)できるようになります。

$acl = new Nette\Security\Permission;

$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // 'registered' は 'guest' を継承します
$acl->addRole('admin', 'registered'); // 'admin' は 'registered' を継承します

次に、利用者が触れられる 資源 の一覧を定めます。

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

資源も継承を使えます。たとえば $acl->addResource('perex', 'article') と書けます。

そしていよいよ大事な部分です。誰が何に対して何をできるかを決める、両者のあいだの決まりを定めます。

// はじめは誰も何もできません

// guest には記事、コメント、アンケートを見せます
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// そしてアンケートへの投票も許します
$acl->allow('guest', 'poll', 'vote');

// registered は guest の権限を継承するので、さらにコメントの権限を与えます
$acl->allow('registered', 'comment', 'add');

// 管理者は何でも見られて編集できます
$acl->allow('admin', $acl::All, ['view', 'edit', 'add']);

誰かがある資源に触れるのを 禁じたい ならどうするでしょうか。

// 管理者はアンケートを編集できません。それは民主的ではないからです
$acl->deny('admin', 'poll', 'edit');

決まりの一そろいができたので、認可の問い合わせを気軽に投げられます。

// guest は記事を見られますか
$acl->isAllowed('guest', 'article', 'view'); // true

// guest は記事を編集できますか
$acl->isAllowed('guest', 'article', 'edit'); // false

// guest はアンケートに投票できますか
$acl->isAllowed('guest', 'poll', 'vote'); // true

// guest はコメントできますか
$acl->isAllowed('guest', 'comment', 'add'); // false

登録した利用者にも同じことが当てはまりますが、この人はコメントもできます。

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

管理者はアンケートを除いてすべてを編集できます。

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

権限は動的にも評価でき、判断をすべてのパラメータが渡される独自のコールバックに任せられます。

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

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

ではロールと資源の名前だけでは足りず、たとえば registered のロールが article の資源を、自分が著者である場合にだけ編集できると定めたいときはどうすればよいでしょうか。文字列の代わりにオブジェクトを使います。ロールは Nette\Security\Roleのオブジェクト、資源は Nette\Security\Resourceのオブジェクトになります。それぞれの getRoleId()getResourceId() メソッドがもとの文字列を返します。

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';
	}
}

そして決まりを作ります。

$assertion = function (Permission $acl, ?string $role, ?string $resource, ?string $privilege): bool {
	$role = $acl->getQueriedRole(); // Registered のオブジェクト
	$resource = $acl->getQueriedResource(); // Article のオブジェクト
	return $role->id === $resource->authorId;
};

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

そして ACL への問い合わせはオブジェクトを渡して行います。

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

ロールはひとつのロールからも複数のロールからも継承できます。しかし、ひとつの祖先ではその操作が禁じられていて、もうひとつでは許されていたらどうなるでしょうか。子孫の権限はどうなるのでしょうか。それはロールの重みが決めます。祖先の並びの最後に書かれたロールがいちばん重く、最初のものがいちばん軽くなります。例で見るとより分かりやすいでしょう。

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

$acl->addResource('backend');

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

// 場合 A: admin のロールは guest のロールより軽い
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false

// 場合 B: admin のロールは guest のロールより重い
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true

ロールと資源は取り除けますし(removeRole()removeResource())、決まりも取り消せます(removeAllow()removeDeny())。直接の親のロールの配列は getRoleParents() が返します。2 つのものが継承の関係にあるかは roleInheritsFrom()resourceInheritsFrom() が返します。ロールや資源があるかは hasRole()hasResource() が調べ、登録されたすべてのロールと資源の一覧は getRoles()getResources() が返し、すべては removeAllRoles()removeAllResources() で消せます。

サービスとして足す

作った ACL は、$user のオブジェクトがそれを使いはじめるように、つまりコードで $user->isAllowed('article', 'view') を使えるように、サービスとして設定へ渡す必要があります。そのためにファクトリを書きます。

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;
	}
}

そして設定に足します。

services:
	- App\Model\AuthorizatorFactory::create

プレゼンターでは、たとえば startup() メソッドで権限を確かめられます。

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