Symfony2有権者とDoctrineフィルター

それはすべて、1つのCRMのセキュリティシステムをセットアップしたときに始まりました。 よくあることですが、メインデータへのアクセスレベルが異なるユーザーがいました(それらをエンティティと呼びましょう)。 メイングリッドのビューは同じで、エンティティへのアクセスに柔軟性が必要でした。 最初はACLについて考えましたが、...



...そのようなことは何も起こりませんでした。



アクセス制御リスト( ACL )は、各ユーザー(またはユーザーのグループ)が各セキュリティオブジェクトで実行できることに関する情報を格納するリストです。 symfonyにはACLメカニズムが組み込まれており、私はそれを学ぶことができました。 まず、オブジェクトに保護をインストールする例を見てみました。 最初の読書では、特権がユーザーにかかっているのが好きではありませんでした。この方法では、巨大な特権テーブルをフェンスし、すべてのユーザーと許可されたアクションをリストする必要があります。 ただし、短いグーグルは、この例に存在するUserSecurityIdentityに加えてRoleSecurityIdentityがあることを教えてくれました 。 いいね! さらに、オブジェクトだけでなくクラスにも特権を掛けることができることを知って嬉しく思いました。 まあ、素晴らしいが、それでも私には合わない。 特権は、エンティティの状態によって異なります。 すべての考えをまとめて、これが将来どのようになるか想像しました:エンティティの作成をキャッチし、エンティティの状態を変更し、データベースにACEを書き込む敗者が作成されます(すべてのロールについて、最初から20以上ありました)。 そして、ユーザーが何かをする必要がある場合、何百万ものACEから1つ(または、ユーザーが複数の役割を持っている場合は複数)を探して、アクションが許可されるようにします。 一般に、システムはかなり面倒で不器用であるように思われましたが、これを取り除くことはできませんが、機能を明確かつ綿密に実行します。



しばらくの間、preferenceとhookersを使用して独自のACL 作成することも検討しましたが、不要なサイクルビルディングとして破棄しました。 この見掛け倒しの唯一のプラス-各ユーザーは多くのロールを持つことができ、累積的な特権が好きだったので、私はマスクを使用することにしました。



私はタスクを中断することを決め、最初にエンティティを取得するときにアクセスを制限することにしました。 その後、私は考えました-そして、すべての基本的なアクセス操作をオーバーライドするカスタムリポジトリを作成します。 しかし、だれかがDQLを使用するか、別のリポジトリから結合を使用してクエリを作成することにした場合、これは私を救いません。 それから、教義の拡張、特に-softdeletableについて思い出しました 。 彼自身は決して役に立たなかった、私は彼がそうだったことを知っていた。



この拡張機能は、多数の接続を持つエンティティを削除する必要がある場合に痛みを和らげることができます(これは常に対症療法であると考え、カスケードを忠実に設定しました)。 不快なエンティティを削除済みとしてマークします。 そしてそれだけです。 それらはデータベースに残りますが、教義はそれらが存在しないふりをします。 しかし、すべて同じように、「削除された」レコードとともに、「すべてを表示」するか、少なくともすべてを表示する機会がありました。



私が必要としたのはこの行動だったので、私は神にオープンソースに感謝し、彼らがそれをどのように変えたかを見るために登りました。 だから私はフィルターについて知りました。



しばらくして、私はそれと一緒に暮らす方法を学び、私の最初のフィルターを書きました。 それから、このフィルターをオンにすることの問題が私に浮かびました。 データを要求するたびにそれを含めるのはうれしくありませんでした-すべてが明確で、私だけでなく、他の開発者、およびこれをサポートすることに携わる貧しい仲間と「うまくいく」必要があります。 どうやってこれに至ったのか覚えていませんが、Configuratorを作成しました。これは、すべてのリクエストで実行され、実際にフィルターをオンにし、同時に必要なパラメーターを置き換えるサービスです。 ビジネスロジックの側面から、通常のクエリを作成し、実行前に、ステータスによって表示されるべきではないすべてのものを遮断するコードをフィルターに追加します。



フィルター自体:



<?php namespace CRMBundle\Entity\Filter; use Doctrine\ORM\Mapping\ClassMetaData; use Doctrine\ORM\Query\Filter\SQLFilter; class EntityFilter extends SQLFilter { public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias) { if ($targetEntity->getName() != 'CRMBundle\Entity\Entity') { return ''; } try { $statuses = $this->getParameter('statuses'); } catch (\InvalidArgumentException $e) { return ''; } if (empty($statuses)) { return ''; } //   -  -   ,     "" $allowedStates = substr($allowedStates, 1, -1); $allowedStates = str_replace('\\', '', $allowedStates); return $targetTableAlias.".status in (".$allowedStates.")"; } }
      
      





コンフィギュレーター:



 <?php namespace CRMBundle\Entity\Filter; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Doctrine\Common\Persistence\ObjectManager; class Configurator { protected $em; protected $tokenStorage; public function __construct(ObjectManager $em, TokenStorageInterface $tokenStorage) { $this->em = $em; $this->tokenStorage = $tokenStorage; } public function onKernelRequest() { if ($user = $this->getUser()) { $entity_filter = $this->em->getFilters()->enable('entity_filter'); //   implode -     $entity_filter->setParameter('allowedStates', "'".implode("', '", $this->getUser()->getAllowedStates('view'))."'"); } } private function getUser() { $token = $this->tokenStorage->getToken(); if (!$token) { return null; } $user = $token->getUser(); if (!($user instanceof UserInterface)) { return null; } return $user; } }
      
      





そして最小構成:



 // config.yml services: doctrine.filter.configurator: class: CRMBundle\Entity\Filter\Configurator arguments: - "@doctrine.orm.entity_manager" - "@security.token_storage" tags: - { name: kernel.event_listener, event: kernel.request } doctrine: orm: filters: entity_filter: class: CRMBundle\Entity\Filter\EntityFilter enabled: false
      
      





その後、たとえば$entity->getChildren();



doctrine.DEBUG: SELECT * FROM entity t0 WHERE t0.parent_id = ? AND ((t0.state in ('new', 'in_work'))) [3]



変わりますdoctrine.DEBUG: SELECT * FROM entity t0 WHERE t0.parent_id = ? AND ((t0.state in ('new', 'in_work'))) [3]



doctrine.DEBUG: SELECT * FROM entity t0 WHERE t0.parent_id = ? AND ((t0.state in ('new', 'in_work'))) [3]







そこで、読み取りアクセスの問題を解決しました。 そして、他のすべてのために、 マスターカード 有権者がいます。 Symfony 2.7での有権者の使用について説明しますが、バージョン2.8では、機能で使用したAbstractVoterに代わるVoterクラスを追加したことに留意してください。



有権者自身が最も簡単です:



 <?php namespace CRMBundle\Security\Authorization\Voter; use Symfony\Component\Security\Core\Authorization\Voter\AbstractVoter; use CRMBundle\Entity\User; use Symfony\Component\Security\Core\User\UserInterface; class EntityVoter extends AbstractVoter { const VIEW = 'view'; const EDIT = 'edit'; const INFO_VIEW = 'info_view'; const INFO_EDIT = 'info_edit'; const ANS_VIEW = 'ans_view'; const ANS_EDIT = 'ans_edit'; const HISTORY = 'history'; protected function getSupportedAttributes() { return array(self::VIEW, self::EDIT, self::INFO_VIEW, self::INFO_EDIT, self::ANS_VIEW, self::ANS_EDIT, self::HISTORY); } protected function getSupportedClasses() { return array('CRMBundle\Entity\Entity'); } protected function isGranted($action, $entity = null, $user = null) { if (!$user instanceof UserInterface) { return false; } if (in_array($entity->getState(), $user->getAllowedStates($action))) { return true; } return false; } }
      
      





そして接続してください:



 // config.yml services: security.access.entity_voter: class: CRMBundle\Security\Authorization\Voter\EntityVoter public: false tags: - { name: security.voter }
      
      





エンティティには13の条件のいずれかがあり、許可が必要な7つのアクションがありました。 これは64ビットのintにも収まらなかったため、各アクションのマスクを作成し、それらのストレージに役割を委任しました。 さらに、エンティティに関連付けられていないグローバル権限も持っていたため、各ロールには合計で8ビットのマスクがありました。 ユーザーのgetMask($アクション)メソッドで、私は彼のすべての役割の目的のマスクに対してビット単位の「and」を実行しました。 マスクは円のように単純です:13ビットは、このマスクが13の可能なエンティティ状態のそれぞれに責任を持つアクションの許可または禁止を反映します。 そこで、ユーザーにgetAllowedStates($ action)メソッドを追加しました。このメソッドは、$ actionが許可されている状態のリストを返します。



 // CRMBundle/Entity/User.php public function getMask($action) { $mask = 0; foreach ($this->userRoles as $role) { $mask = $mask | $role->getMask($action); } return $mask; } public function getAllowedStates($action = 'view') { $result = []; $mask = $this->getMask($action); foreach (['new', 'in_work', 'etc.'] as $key => $value) { if (((1 << $key) & $mask) != 0) { $result[] = $value; } } return $result; } // controller $this->denyAccessUnlessGranted('info_view', $entity, '  !');
      
      





簡単な場合:アプリケーションのキーポイントで、Entityでアクションを実行する前に、denyAccessUnlessGranted($アクション、$エンティティ、$ decayMessage)を呼び出し、実行するアクション、このアクションが実行されるエンティティ、およびメッセージを渡します。失敗した場合、Exceptionによってスローされます。 とても簡単です。 カスタム投票者を作成するときは、チェックするエンティティのアクションとタイプを指定します。 有権者の中では、$の実体で$のアクションを実行したいユーザーへのアクセスがあります。 したがって、必要なものはすべてあり、現在のユーザーの応答getAllowedStates($アクション)で$エンティティの現在の状態を確認し、結果を返す必要がありました。



まとめると。 Doctrine Filterを使用してデータのアクセス制御を実装しました。 このようなフィルターは、選択したエンティティへのすべてのクエリに条件を追加できます。 このソリューションの利点は、すべてを機能させるためにビジネスロジックの部分で修正が必要ないことです。 Symfony Voterによる他のアクションの許可の検証を実装しました。



All Articles