DeclarativePolicy 프레임워크

DeclarativePolicy 프레임워크는 정책 확인의 성능을 지원하고 EE의 확장을 용이하게 하는 데 의도되었습니다. app/policies의 DSL 코드는 Ability.allowed?에서 특정 작업을 특정 주제에 대해 허용할 수 있는지 확인하는 데 사용됩니다.

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

Ruby 젬 소스는 declarative-policy GitLab 프로젝트에서 사용할 수 있습니다.

권한 규칙 관리

권한은 conditionsrules 두 부분으로 나뉩니다. Conditions은 데이터베이스 및 환경에 액세스할 수 있는 부울 표현식이며, rules는 정적으로 구성된 표현식 및 다른 규칙의 조합으로 특정 능력을 활성화하거나 방지합니다. 허용되는 능력은 적어도 하나의 규칙에 의해 활성화되어야 하며 아무 것도 방해받지 않아야 합니다.

Conditions

Conditions은 condition 메서드로 정의되며 이름과 블록이 주어집니다. 블록은 정책 개체의 컨텍스트에서 실행되므로 @user@subject에 액세스할 수 있으며, 정책에서 정의된 모든 메서드를 호출할 수 있습니다. @user는 nil일 수 있지만 @subject는 주제 클래스의 실제 인스턴스임이 보장됩니다.

class FooPolicy < BasePolicy
  condition(:is_public) do
    # @subject guaranteed to be an instance of Foo
    @subject.public?
  end

  # 인스턴스 메서드도 조건에서 호출할 수 있습니다
  condition(:thing) { check_thing }

  def check_thing
    # ...
  end
end

조건을 정의하면 정책에 조건이 통과하는지 확인하는 프레디케이트 메서드가 정의됩니다. 따라서 위의 예제에서 FooPolicy의 인스턴스는 #is_public?#thing?에 응답합니다.

조건은 그들의 범위에 따라 캐시됩니다. 범위 및 순서에 대해서는 나중에 다루겠습니다.

Rules

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 내에서 다음을 사용할 수 있습니다.

  • 일반 단어는 이름에 의해 조건을 언급합니다 - 해당 조건이 진실 일 때 적용되는 규칙입니다.
  • ~는 부정을 나타내며, negate로도 사용할 수 있습니다.
  • &|은 논리적 조합이며, all?(...)any?(...)로도 사용할 수 있습니다.
  • can?(:other_ability):other_ability에 적용되는 규칙을 위임합니다. 이는 동적으로 확인할 수 있는 인스턴스 메서드 can?과 구분됩니다. 이는 다른 능력에 대한 위임을 구성하는 것으로, 이것은 다른 능력에 대한 동적 확인을 할 수 있습니다.

~, &, 및 | 연산자는 DeclarativePolicy::Rule::Base에서 재정의된 메서드입니다.

규칙 DSL 내에서 불리언 연산자인 &&||를 사용하지 마십시오. 규칙 블록 내의 조건은 불리언이 아닌 객체이기 때문에 이러한 연산자 및 삼항 연산자 (condition ? ... : ...) 및 if 블록을 사용할 수 없습니다. 이러한 연산자는 재정의될 수 없으므로 사용자 정의 cop를 통해 금지됩니다.

점수, 순서, 성능

규칙이 어떻게 판단으로 평가되는지 보려면 레일스 콘솔을 열고 다음을 실행하세요: 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 호출로 인해, 부모가 싫어하는 경우에도 자녀가 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를 충족시키지 않지만 다른 능력을 위해 사용할 수 있습니다. 자녀 정책은 그런 다음 Child에게 유용하고 Parent에게 유용하지 않은 방식으로 :eat_broccoli를 정의할 수 있습니다.

overrides를 사용하는 대안

overrides를 무시하는 것은 복잡합니다. 똑같은 이유 때문에, 위임이 복잡하기 때문에입니다. 논리적 추론에 대해 이해하고 의미를 명확하게 하는 것을 포함하고 있기 때문입니다. override의 남용은 코드의 중복 및 잠재적인 보안 버그 도입의 가능성이 있으며, 방지해야 할 것을 허용할 수 있습니다. 그 이유 때문에, 이는 다른 방법이 적용 불가능한 경우에만 사용되어야 합니다.

다른 방법으로는 예를 들어 다른 능력 이름을 사용하는 것이 포함될 수 있습니다. 음식을 선택하는 것과 받은 음식을 먹는 것은 의미론적으로 구별되며 다른 이름이 붙을 수 있습니다(이 경우에는 chooses_to_eat_broccolieats_what_is_given일 수 있습니다). 호출 지점이 다형적인지에 따라 다를 수 있습니다. Parent 또는 Child로 항상 정책을 확인하는 것을 알고 있다면 적절한 능력 이름을 선택할 수 있습니다. 호출 지점이 다형적이라면 그것은 불가능합니다.

정책 클래스 지정

주어진 주체에 대한 사용되는 정책을 무시할 수도 있습니다:

class Foo

  def self.declarative_policy_class
    'SomeOtherPolicy'
  end
end

이것은 일반적으로 계산된 FooPolicy 클래스 대신 SomeOtherPolicy 클래스를 사용하고 권한을 확인합니다.