DeclarativePolicy 프레임워크

DeclarativePolicy 프레임워크는 정책 확인을 수행하고 EE의 확장성을 위해 쉽게 활용하기 위해 설계되었습니다. app/policies의 DSL 코드는 Ability.allowed?가 주체에 대한 특정 동작이 허용되는지 확인하는 데 사용됩니다.

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

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

명명 및 규칙에 대한 정보는 Permission conventions page를 참조하십시오.

권한 규칙 관리

권한은 conditionsrules로 분할됩니다. Conditions은 데이터베이스와 환경에 액세스할 수 있는 논리적 표현식입니다. 반면에 rules는 정적으로 구성된 표현식 및 다른 규칙의 결합으로 특정 능력을 활성화하거나 방지하는 데 사용됩니다. 특정 능력을 허용하려면 적어도 하나의 규칙으로 활성화해야 하며, 방지되지 않아야 합니다.

Conditions

Conditions은 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?에도 응답합니다.

조건은 스코프에 따라 캐시됩니다. 스코프와 순서에 대해서는 나중에 다룰 것입니다.

Rules

rule은 조건 및 다른 규칙의 논리적 조합으로, 특정 능력을 활성화하거나 방지하기 위해 구성된 것입니다. 규칙 구성은 정적이며, 규칙의 논리는 데이터베이스에 접근하거나 @user 또는 @subject에 대해 알 수 없습니다. 이는 우리가 조건 레벨에서만 캐시할 수 있도록 함으로써 가능합니다. 규칙은 DSL 구성의 블록을 가져오는 rule 메서드를 통해 지정되며, #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를 통해 금지되었습니다.

스코어, 순서, 성능

규칙이 판단으로 어떻게 평가되는지 확인하려면 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)), 정책은 정책에서 모든 조건을 계산할 필요는 없습니다. 먼저 해당 특정 능력에 적용되는 규칙만 선택됩니다. 그런 다음 실행 모델은 단락 평가(short-circuiting)를 활용하고, 얼마나 계산하기 비용이 비싼지에 대한 휴리스틱을 기준으로 규칙을 정렬하려고 합니다. 정렬은 동적이며 캐시 인식형이므로 이전에 계산된 조건이 다른 조건을 계산하기 전에 먼저 고려됩니다.

스코어는 개발자가 다른 규칙들에 비해 이 규칙을 평가하는 것이 얼마나 비용이 많이 들지를 나타내기 위해 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 호출을 제거할 수 있지만, 여전히 도움이 되지 않습니다. 규칙이 다르기 때문입니다: 부모는 좋아하는 대로 먹을 수 있고, 자식은 행동이 좋다면 주어진 것을 먹을 수 있습니다. 위임을 허용하면 부모가 녹색 야채를 좋아하는 아이들만 먹게 될 것입니다. 그러나 부모는 아이에게 브로콜리를 먹이겠지요, 스스로 싫어하지만 아이에게 좋기 때문입니다.

해결책은 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

이 정의를 사용하면 ChildPolicyParentPolicy에서 :eat_broccoli를 충족시키기 위해서 전혀 찾지 않지만 다른 능력에 대해서는 사용합니다. 그런 다음 자식 정책은 Parent에게 그리고 Child에게 의미 있는 방식으로 :eat_broccoli를 정의할 수 있습니다.

overrides 사용 대안

정책 위임을 무효화하는 것은 정책 위임과 마찬가지로 복잡합니다. 이것은 논리적 추론에 대한 추론과 의미에 대해 명확해야 하기 때문입니다. overrides의 오용은 코드를 복제하거나 방지해야 하는 것을 허용하여 보안 버그를 가능성 있게 할 수 있습니다. 이러한 이유로 다른 방법이 실행 가능하지 않을 때에만 사용해야 합니다.

예를 들면 능력 이름을 다르게 지정하는 것이 있을 수 있습니다. 음식을 선택하고 주어진 음식을 먹는 것은 의미적으로 다르며, 이들은 다른 이름(chooses_to_eat_broccolieats_what_is_given과 같이)으로 지정될 수 있습니다. 이것은 호출 사이트가 얼마나 다형적인가에 따라 다를 수 있습니다. ParentChild로 항상 정책을 확인하는 것을 알고 있다면 적절한 능력 이름을 선택할 수 있습니다. 호출 사이트가 다형적이라면 그렇게 할 수 없습니다.

정책 클래스 지정

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

class Foo

  def self.declarative_policy_class
    'SomeOtherPolicy'
  end
end

이는 평소 계산된 FooPolicy 클래스 대신 SomeOtherPolicy 클래스를 사용하여 권한을 사용하고 확인합니다.