Do You PHP はてブロ

Do You PHPはてなからはてブロに移動しました

Symfony2で権限の組み合わせを満たす場合のみアクセスを許可したい

Symfony2では権限によるアクセス制御はapp/config/security.ymlなどにある"access_control"で指定しますが、直下のrolesには複数の権限が設定できます。

security:
    access_control:
        - { path: ^/foo/bar/, roles: [ROLE_A, ROLE_B]}

ただし、このrolesって"OR条件"なので、上の例の場合だとROLE_AかROLE_Bを持っていれば"/foo/bar/"にアクセスできます。
で、rolesをAND条件にする方法はないかと


security.ymlのsecurity.access_control.rolesに複数書いた時はOR条件なのか。AND条件にならんのかな?

とツイートしたところ、id:Fivestarさんから


@shimooka そしたらsecurity.access.simple_role_voterサービスを上書きするとかですかねw

というリプライをもらってから試行錯誤していたところ、


@shimooka こんな感じでクラス定義して、DIコンテナのパラメータ書き換えるだけでできると思います(実装は確認してないですが...) gist.github.com/1709098

というサンプルを提示してもらいました。で、


@fivestr おお、似たようなことやってる途中でしたw うまくいったらまとめときます

とりあえず、期待する動作をするようになったので一度まとめておきます。間違いなどあれば指摘してください:-)

前提

  • ユーザーは1つ以上の権限を持つ場合がある
  • 特定のURLに対して、権限の組み合わせを満たす場合のみアクセスを許可したい

環境

  • PHP5.3.9
  • Symfony2.0.9 without venders
  • ソースはgithubに上げてあります
    • Acme/AndRoleVoterBundle
    • app/config/parameters.iniがありませんので、試す場合は別途作成してください

手順1:security.ymlのaccess_controlを定義する

冒頭にも書きましたが、security.access_controlのrolesに複数指定します。

security:access_control:
        - { path: ^/AndRoleVoterBundle/user/, roles: [ROLE_A]}
        - { path: ^/AndRoleVoterBundle/private/, roles: [ROLE_A, ROLE_B]}

また、サンプルでは以下のユーザーも併せて追加してます。

ユーザー名 権限
role_a ROLE_A
role_both ROLE_A、ROLE_B

github上のソースは以下のとおりです。

手順2:RoleHierarchyVoter#voteのロジックを変更する

id:Fivestarさんからはsecurity.access_control.roles(Symfony\Component\Security\Core\Authorization\Voter\RoleVoter.php)という情報をもらいましたが、このクラスには権限があるかどうかを決定するvoteメソッドがあります。で、実際にはRoleVoterクラスを変更するのではなく、そのサブクラスであるsecurity.access.role_hierarchy_voter.class(Symfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter.php)が利用されているようでした。RoleHierarchyVoterクラスはvoteメソッドをオーバーライドしていません。
ということで、次のような感じでRoleHierarchyVoterクラスのサブクラスを作り、voteメソッドをオーバーライドします。

<?php

namespace Acme\AndRoleVoterBundle\Component\Security\Authorization\Voter;

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

class AndRoleHierarchyVoter extends RoleHierarchyVoter
{
    /**
     * {@inheritdoc}
     */
    public function vote(TokenInterface $token, $object, array $attributes)
    {
        $satisfied_roles = true;
        $roles = $this->extractRoles($token);
        foreach ($attributes as $attribute) {
            if (!$this->supportsAttribute($attribute)) {
                continue;
            }
            $has_role = false;
            foreach ($roles as $role) {
                if ($attribute === $role->getRole()) {
                    $has_role = true;
                }
            }
            $satisfied_roles &= $has_role;
        }
        return ($satisfied_roles ? VoterInterface::ACCESS_GRANTED : VoterInterface::ACCESS_DENIED);
    }
}

github上のソースは以下のとおりです。

手順3:サービスのパラメータの上書き設定を追加する

アクセス制限をかけるBundleのResources/config/services.xmlに追加します。

<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <parameters>
        <parameter key="security.access.role_hierarchy_voter.class">Acme\AndRoleVoterBundle\Component\Security\Authorization\Voter\AndRoleHierarchyVoter</parameter>
    </parameters>
</container>

github上のソースは以下のとおりです。実際にはservices.xmlとなります。

で。。。

最後のservices.xmlですが、services.ymlでも動作するハズと思い試してみたのですがxml形式でないと動作しませんでした。。。


@shimooka バンドルのResources/configにあるサービス定義ファイルは、バンドルのDependencyInjection/**Extension.php内で読み込んでいます。形式ごとのローダーでファイルを読み込んでいるだけなので、app側とは独立しています

ちなみに、Entityを使った認証するBundle環境だとservices.xmlでもservices.ymlファイルでもちゃんと動作してるんですよねぇ。。。なんだろ。。。

追記(2012/02/02 11:35)

@hidenorigotoさんからもらったpullリクエストをマージしました。ありがとうございます:-)

現在のソースではymlファイルでもDIコンテナのパラメータ(security.access.role_hierarchy_voter.class)を上書きできます。

なぜこのような違いになったかは次のエントリで