DeclarativePolicy 프레임워크

DeclarativePolicy 프레임워크는 정책 확인의 성능을 지원하고 EE의 확장을 용이하게 하는 것을 목표로 합니다. app/policies의 DSL 코드는 Ability.allowed?가 특정 작업이 주제에서 허용되는지 확인하는 데 사용됩니다.

사용된 정책은 주제의 클래스 이름에 기반하여 만들어집니다. 따라서 Ability.allowed?(user, :some_ability, project)ProjectPolicy를 생성하고 해당 권한을 확인합니다.

루비 젬 소스는 declarative-policy GitLab 프로젝트에서 확인할 수 있습니다.

권한 규칙 관리

권한은 conditionsrules 두 부분으로 나뉩니다. 조건은 데이터베이스와 환경에 액세스할 수 있는 부울 표현식으로, 규칙은 특정 기능을 가능하게 하거나 방지하는 데 사용되는 정적으로 구성된 표현식 및 다른 규칙의 조합입니다. 허용되는 기능이 있기 위해서는 적어도 하나의 규칙에서 활성화되어야하며, 어떤 것에도 방지되어서는 안 됩니다.

조건

조건은 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))

각 행은 평가된 규칙을 나타냅니다. 주목할 점은 다음과 같습니다:

  1. - 기호는 규칙 블록이 false로 평가되었음을 나타냅니다. + 기호는 규칙 블록이 true로 평가되었음을 나타냅니다.
  2. 대괄호 내의 숫자는 점수를 나타냅니다.
  3. 행의 마지막 부분(예: @john : Issue/1)은 해당 규칙에 대한 사용자 이름 및 주제를 보여줍니다.

여기서 처음 네 가지 규칙이 어떤 사용자 및 주제에 대해 false로 평가되었음을 볼 수 있습니다. 예를 들어, 마지막 행에서 사용자 johnProject/4에서 기자 역할을 가졌기 때문에 규칙이 활성화된 것을 볼 수 있습니다.

정책이 특정 기능이 허용되는지(policy.allowed?(:some_ability)) 묻힐 때, 정책의 모든 조건을 계산할 필요가 없을 수 있습니다. 먼저 해당하는 특정 기능에 관련된 규칙만 선택됩니다. 그런 다음 실행 모델은 단락평가를 활용하고, 어떤 조건이 계산하는 데 얼마나 비용이 드는지에 대한 휴리스틱을 기반으로 규칙을 정렬하려고 합니다. 정렬은 동적이며 캐시를 고려하여 이전에 계산된 조건이 계산되기 전에 고려됩니다.

점수는 개발자가 conditionscore: 매개변수를 통해 선택합니다.

범위

때로는 조건이 @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 클래스에서 사용하고 권한을 확인합니다.