DeclarativePolicy
フレームワーク
DeclarativePolicyフレームワークは、ポリシー・チェックのパフォーマンスを支援し、EEの拡張を容易にするために設計されています。app/policies
の DSL コードは、Ability.allowed?
が、特定のアクションがサブジェクトで許可されているかどうかをチェックするために使用するものです。
使用されるポリシーは、サブジェクトのクラス名に基づいています。したがって、Ability.allowed?(user, :some_ability, project)
は、ProjectPolicy
を作成し、その権限をチェックします。
Ruby gemのソースはdeclarative-policyGitLabプロジェクトにあります。
権限ルールの管理
権限はconditions
とrules
の2つの部分に分けられます。Conditionsはデータベースと環境にアクセスできるブーリアン式で、Rulesは特定の能力を有効または無効にする式と他のルールの静的に設定された組み合わせです。ある能力が許可されるためには、少なくとも1つのルールによって有効化され、どのルールによっても阻止されない必要があります。
条件
条件はcondition
メソッドで定義され、名前とブロックが与えられます。このブロックは、ポリシーオブジェクトのコンテキストで実行されます。したがって、@user
と@subject
にアクセスし、ポリシーで定義されたメソッドを呼び出すことができます。@user
は (匿名の場合) nil になる可能性がありますが、@subject
はサブジェクトクラスの実際のインスタンスであることが保証されます。
class FooPolicy < BasePolicy
condition(:is_public) do
# @subject guaranteed to be an instance of Foo
@subject.public?
end
# instance methods can be called from the condition as well
condition(:thing) { check_thing }
def check_thing
# ...
end
end
条件を定義すると、その条件がパスするかどうかをチェックするための述語メソッドがポリシー上に定義されます。したがって、上記の例では、FooPolicy
のインスタンスも#is_public?
と#thing?
に応答します。
条件はそのスコープに従ってキャッシュされます。スコープと順序については後で説明します。
ルール
rule
は条件と他のルールの論理的な組み合わせで、特定の能力を有効または無効にするように設定されます。ルールの設定は静的であり、ルールのロジックがデータベースに触れたり、@user
や@subject
について知ることはできません。これにより、条件レベルでのキャッシュのみが可能になります。ルールはrule
メソッドで指定します。このメソッドはDSL設定のブロックを受け取り、#enable
または#prevent
に応答するオブジェクトを返します:
class FooPolicy < BasePolicy
# ...
rule { is_public }.enable :read
rule { thing }.prevent :read
# equivalently,
rule { is_public }.policy do
enable :read
end
rule { ~thing }.policy do
prevent :read
end
end
ルールDSL内では、以下を使用することができます:
- 規則 DSL の中では、次のようなものを使うことができます: 条件を指定する通常の単語 - 条件が真であるときに有効になる規則。
-
~
は否定を表し、negate
もあります。 -
&
と|
は論理の組み合わせで、all?(...)
とany?(...)
としても利用できます。 -
can?(:other_ability)
:other_ability
これは、動的にチェックできるインスタンスメソッドcan?
とは異なります。
~
&
および|
オペレータは、DeclarativePolicy::Rule::Base
でオーバーライドされたメソッドです。
ルールブロック内の条件はオブジェクトであり、ブーリアンではないため、&&
や||
のようなブーリアン演算子をルールDSL内部で使用しないでください。三項演算子 (condition ? ... : ...
) やif
ブロックも同様です。これらのオペレーションはオーバーライドできないため、カスタム・コップで禁止されています。
スコア、順序、パフォーマンス
ルールがどのように評価されて判定になるかを見るには、Railsコンソールを開いて次のように実行します:policy.debug(:some_ability)
.これにより、ルールが評価される順番に表示されます。
たとえば、IssuePolicy
をデバッグしたいとします。このようにデバッガを実行します:
user = User.find_by(username: 'john')
issue = Issue.first
policy = IssuePolicy.new(user, issue)
policy.debug(:read_issue)
デバッグ出力の例は次のようになります:
- [0] prevent when all?(confidential, ~can_read_confidential) ((@john : Issue/1))
- [0] prevent when archived ((@john : Project/4))
- [0] prevent when issues_disabled ((@john : Project/4))
- [0] prevent when all?(anonymous, ~public_project) ((@john : Project/4))
+ [32] enable when can?(:reporter_access) ((@john : Project/4))
各行は評価されたルールを表します。注意すべき点がいくつかあります:
-
-
の記号は、ルールブロックがfalse
と評価されたことを示します。+
の記号は、ルール・ブロックがtrue
と評価されたことを示します。 - 括弧内の数字はスコアを示します。
- 行の最後の部分(たとえば、
@john : Issue/1
)は、そのルールのユーザー名と件名を示します。
ここでは、最初の4つのルールが、どのユーザとサブジェクトに対してfalse
評価されたかがわかります。例えば、最後の行では、ユーザjohn
がProject/4
のレポーター・ロールを持っていたため、ルールがアクティビティされたことがわかります。
ポリシーが特定の能力を許可するかどうか尋ねられるとき (policy.allowed?(:some_ability)
)、必ずしもポリシーのすべての条件を計算する必要はありません。まず、その特定の能力に関連するルールだけが選択されます。次に、実行モデルは短絡を利用し、計算コストがどの程度高いかをヒューリスティックに判断してルールを並べ替えようとします。並べ替えは動的でキャッシュを意識して行われるため、他の条件を計算する前に、以前に計算された条件が最初に考慮されます。
スコアは、開発者がcondition
のscore:
パラメータで選択します。これは、このルールを評価することが、他のルールと比較してどれだけコストがかかるかを示すものです。
スコープ
時には、@user
のデータのみ、あるいは@subject
のデータのみを使用する条件もあります。この場合、不必要に条件を再計算しないように、キャッシュのスコープを変更したいと思います。例えば
class FooPolicy < BasePolicy
condition(:expensive_condition) { @subject.expensive_query? }
rule { expensive_condition }.enable :some_ability
end
単純に、Ability.allowed?(user1, :some_ability, foo)
とAbility.allowed?(user2, :some_ability, foo)
を呼び出した場合、条件を2回計算しなければなりません。しかし、scope: :subject
オプションを使用すると、次のようになります:
condition(:expensive_condition, scope: :subject) { @subject.expensive_query? }
を使うと、条件の結果は件名に基づいてのみグローバルにキャッシュされるので、異なるユーザーに対して繰り返し計算されることはありません。同様に、scope: :user
はユーザーに基づいてのみキャッシュされます。
危険: 条件が実際にユーザーとサブジェクトの両方のデータを使用する (単純な匿名チェックを含む!) 場合に:scope
オプションを使用すると、結果があまりにもグローバルなスコープでキャッシュされ、キャッシュバグが発生します。
一つのサブジェクトに対して多くのユーザの権限をチェックしたり、一つのユーザに対して多くのサブジェクトの権限をチェックしたりすることがあります。この場合、優先スコープを設定します。つまり、繰り返されるパラメータにキャッシュされるルールを優先することをシステムに伝えます。例えば、Ability.users_that_can_read_project
:
def users_that_can_read_project(users, project)
DeclarativePolicy.subject_scope do
users.select { |u| allowed?(u, :read_project, project) }
end
end
これは例えば、user.admin?
をチェックするよりも、project.public?
をチェックすることを優先します。
委任
委任とは、別のポリシーから、別の主題に関するルールを含めることです。例えば
class FooPolicy < BasePolicy
delegate { @subject.project }
end
には、ProjectPolicy
からのすべてのルールが含まれます。 委任された条件は、正しい委任対象で評価され、ポリシー内の通常のルールと一緒に並べ替えられます。特定の能力に関連するルールだけが実際に考慮されます。
オーバーライド
私たちは、ポリシーが委任された能力をオプトアウトすることを許可します。
委任されたポリシーは、委任するポリシーにとって正しくない方法でいくつかの能力を定義することがあります。たとえば子供と親の関係では、推論できる能力とできない能力があります:
class ParentPolicy < BasePolicy
condition(:speaks_spanish) { @subject.spoken_languages.include?(:es) }
condition(:has_license) { @subject.driving_license.present? }
condition(:enjoys_broccoli) { @subject.enjoyment_of(:broccoli) > 0 }
rule { speaks_spanish }.enable :read_spanish
rule { has_license }.enable :drive_car
rule { enjoys_broccoli }.enable :eat_broccoli
rule { ~enjoys_broccoli }.prevent :eat_broccoli
end
ここで、子ポリシーを親ポリシーに委譲した場合、いくつかの値は正しくないでしょう - 子が親の言語を話すことができると正しく推論できるかもしれませんが、親が運転できるからといって、子が運転できると推論したり、ブロッコリーを食べると推論したりするのは正しくないでしょう。
このようなことの中には、対処できるものもあります。例えば、子供の方針で、普遍的に車の運転を禁止することができます:
class ChildPolicy < BasePolicy
delegate { @subject.parent }
rule { default }.prevent :drive_car
end
しかし、食べ物の嗜好のほうは難しいです - 親ポリシーでprevent
を呼び出すので、親がそれを嫌えば、子ポリシーでenable
を呼び出しても、:eat_broccoli
は有効になりません。
親ポリシーのprevent
呼び出しを削除することもできますが、それでも役に立ちません。ルールが違うからです。親は好きなものを食べることができ、子どもは行儀よくしていれば、与えられたものを食べることができます。親が好きなものを食べ、子供は与えられたものを食べるのです。しかし、親は、たとえ自分が嫌いな野菜であっても、子どものためになるからとブロッコリーを与えるかもしれません。
解決策は、子ポリシーで:eat_broccoli
の能力を上書きすることです:
class ChildPolicy < BasePolicy
delegate { @subject.parent }
overrides :eat_broccoli
condition(:good_kid) { @subject.behavior_level >= Child::GOOD }
rule { good_kid }.enable :eat_broccoli
end
この定義では、ChildPolicy
は、ParentPolicy
を満たすために、:eat_broccoli
を :eat_broccoli
参照する:eat_broccoli
ことは :eat_broccoli
ありません。:eat_broccoli
子ポリシーは :eat_broccoli
Parent
ではなくChild
に対して意味のある方法で:eat_broccoli
定義することができます :eat_broccoli
。
を使用する代替方法overrides
ポリシーの委譲を上書きすることは、委譲が複雑であるのと同じ理由で、複雑です - 論理的な推論を推論し、セマンティクスを明確にする必要があります。override
を誤用すると、コードが重複したり、セキュリティバグが発生する可能性があります。このような理由から、他のアプローチが実行不可能な場合にのみ使用されるべきです。
他のアプローチとしては、例えば、異なる能力名を使うことなどが考えられます。食べ物を食べることを選択することと、与えられた食べ物を食べることは、意味的に異なるものであり、異なる名前にすることができます(この場合、おそらくchooses_to_eat_broccoli
とeats_what_is_given
)。呼び出し先がどの程度多型であるかにもよります。常にParent
またはChild
でポリシーをチェックすることがわかっていれば、適切な能力名を選択できます。呼び出し先が多形である場合、そのようなことはできません。
ポリシー・クラスの指定
指定したサブジェクトに使用する Policy をオーバーライドすることもできます:
class Foo
def self.declarative_policy_class
'SomeOtherPolicy'
end
end
これは、通常の計算されたFooPolicy
クラスではなく、SomeOtherPolicy
クラスの権限を使用し、チェックします。