DeclarativePolicy
프레임워크
DeclarativePolicy 프레임워크는 정책 확인의 성능을 지원하고 EE의 확장을 용이하게 하는 것을 목표로 합니다. app/policies
의 DSL 코드는 Ability.allowed?
가 특정 작업이 주제에서 허용되는지 확인하는 데 사용됩니다.
사용된 정책은 주제의 클래스 이름에 기반하여 만들어집니다. 따라서 Ability.allowed?(user, :some_ability, project)
는 ProjectPolicy
를 생성하고 해당 권한을 확인합니다.
루비 젬 소스는 declarative-policy GitLab 프로젝트에서 확인할 수 있습니다.
권한 규칙 관리
권한은 conditions
과 rules
두 부분으로 나뉩니다. 조건은 데이터베이스와 환경에 액세스할 수 있는 부울 표현식으로, 규칙은 특정 기능을 가능하게 하거나 방지하는 데 사용되는 정적으로 구성된 표현식 및 다른 규칙의 조합입니다. 허용되는 기능이 있기 위해서는 적어도 하나의 규칙에서 활성화되어야하며, 어떤 것에도 방지되어서는 안 됩니다.
조건
조건은 condition
메서드에 의해 정의되며 이름과 블록이 주어집니다. 블록은 정책 객체의 컨텍스트에서 실행되므로 @user
및 @subject
에 액세스할 수 있으며 정책에서 정의된 모든 메서드를 호출할 수 있습니다. @user
는 nil이 될 수 있지만, @subject
는 주제 클래스의 실제 인스턴스임이 보장됩니다.
class FooPolicy < BasePolicy
condition(:is_public) do
# @subject는 Foo의 인스턴스임이 보장됩니다
@subject.public?
end
# 인스턴스 메서드는 조건에서도 호출될 수 있습니다
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
# 동등하게,
rule { is_public }.policy do
enable :read
end
rule { ~thing }.policy do
prevent :read
end
end
규칙 DSL 내에서 다음을 사용할 수 있습니다:
- 일반 단어는 이름으로 조건을 언급합니다 - 해당 조건이 true일 때 유효한 규칙입니다.
-
~
는 부정을 나타내며,negate
로도 사용할 수 있습니다. -
&
및|
은 논리적인 조합으로,all?(...)
및any?(...)
로도 사용할 수 있습니다. -
can?(:other_ability)
은:other_ability
에 적용되는 규칙에 위임합니다. 이는 동적으로 확인할 수 있는 인스턴스 메서드can?
과는 다릅니다 - 이것은 다른 기능에 대한 위임을 구성합니다.
~
, &
, |
연산자는
DeclarativePolicy::Rule::Base
에서 재정의된 메서드입니다.
규칙 DSL 내에서 불리언 연산자인 &&
및 ||
와 같은 연산자는 사용하지 않아야 합니다. 이는 규칙 블록 내의 조건이 부울이 아니라 객체이기 때문입니다. 같은 이유로 삼항 연산자 (condition ? ... : ...
) 및 if
블록도 사용하면 안 됩니다. 이러한 연산자들은 재정의할 수 없으며, 따라서 사용자 지정 cop를 통해 금지되어 있습니다.
점수, 순서, 성능
규칙이 판단으로 어떻게 평가되는지 알아보려면 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
)은 해당 규칙에 대한 사용자 이름 및 주제를 보여줍니다.
여기서 처음 네 가지 규칙이 어떤 사용자 및 주제에 대해 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)
를 호출하는 경우 다른 사용자를 위한 것이기 때문에 조건을 두 번 계산해야합니다. 그러나 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
예를 들어, 이 경우 project.public?
를 확인하는 것보다 user.admin?
를 확인하는 것을 선호합니다.
위임
위임은 다른 주제에서 다른 정책을 포함하는 것입니다. 예를 들어:
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
호출을 제거할 수 있지만, 아이가 부모의 정책에서 이를 먹는 것은 가능합니다. 이 문제를 해결하기 위해서 ChildPolicy
에서 :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
를 확인하지 않지만 다른 능력에는 사용할 수 있습니다. 아이 정책은 Child
에게 맞고 Parent
에게 맞지 않는 방식으로 :eat_broccoli
를 정의할 수 있습니다.
overrides
사용의 대체안
정책 위임을 재정의하는 것은 정책 위임이 복잡하기 때문에 복잡합니다. 논리적 추론에 대한 추론과 의미에 대해 명확해야 합니다. override
의 남용은 코드를 중복시킬 수 있으며 보안 버그를 도입할 수 있기 때문에 다른 방법이 불가할 때에만 사용해야 합니다.
다른 방법은 다른 능력 이름을 사용하는 것일 수 있습니다. 음식을 선택하거나 주어진 음식을 먹는 것을 선택하는 것은 의미론적으로 구별되므로 다른 이름을 지정할 수 있습니다. 이는 호출 부위가 다형성적인지에 따라 다를 수 있습니다. Parent
또는 Child
로 항상 정책을 확인하는 경우 적절한 능력 이름을 선택할 수 있습니다. 호출 부위가 다형성적이라면 그렇게 할 수 없습니다.
정책 클래스 지정
지정된 주제에 대한 정책을 재정의할 수 있습니다.
class Foo
def self.declarative_policy_class
'SomeOtherPolicy'
end
end
이렇게 하면 보통의 계산된 FooPolicy
클래스 대신 SomeOtherPolicy
클래스에서 사용하고 권한을 확인합니다.