DeclarativePolicy 프레임워크

DeclarativePolicy 프레임워크는 정책 검사를 수행하는 데 도움을 주고 EE(Enterprise Edition) 확장을 용이하게 하기 위해 설계되었습니다. app/policies의 DSL 코드가 Ability.allowed?에서 특정 행동이 주제에 대해 허용되는지 확인하는 데 사용됩니다.

정책은 주제의 클래스 이름을 기반으로 하므로, Ability.allowed?(user, :some_ability, project)ProjectPolicy를 생성하고 해당 권한을 확인합니다.

Ruby gem 소스는 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에 대해 알 수 없습니다. 이를 통해 조건 수준에서만 캐시할 수 있습니다. 규칙은 블록의 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 블록도 마찬가지입니다. 이러한 연산자는 재정의할 수 없으므로 커스텀 코드에서 금지됩니다.

점수, 순서, 성능

규칙이 판단으로 평가되는 방식을 보려면, 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에서 Reporter 역할을 가지고 있기 때문에 규칙이 활성화되었음을 알 수 있습니다.

정책에 특정 능력이 허용되는지 문의할 때(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

예를 들어, 이는 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를 충족시키지 않지만, 다른 능력에 대해서는 사용 합니다. 자식 정책은 이제 Child에 맞는 방식으로 :eat_broccoli를 정의할 수 있으며, Parent에는 맞지 않습니다.

overrides 사용의 대안

정책 위임을 재정의하는 것은 복잡합니다. 이는 위임이 복잡한 이유와 같습니다 - 논리적 추론에 대한 사고와 의미에 대해 명확하게 하는 것이 포함됩니다. override의 잘못된 사용은 코드를 복제하고, 방지해야 할 것을 허용하는 보안 버그를 도입할 수 있습니다. 이러한 이유로, 다른 접근이 불가능할 때만 사용해야 합니다.

다른 접근 방식으로는 예를 들어 다른 능력 이름을 사용하는 것이 있을 수 있습니다. 음식을 선택하여 먹는 것과 주어진 음식을 먹는 것은 의미적으로 구별되며, (여기서 chooses_to_eat_broccolieats_what_is_given과 같이) 다르게 명명할 수 있습니다. 이것은 호출 사이트가 얼마나 다형적인지에 따라 달라질 수 있습니다. 항상 Parent 또는 Child로 정책을 확인하는 경우에는 적절한 능력 이름을 선택할 수 있습니다. 호출 사이트가 다형적인 경우에는 그렇게 할 수 없습니다.

정책 클래스 지정

주어진 주제에 대해 사용되는 정책을 재정의할 수도 있습니다:

class Foo

  def self.declarative_policy_class
    'SomeOtherPolicy'
  end
end

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