DeclarativePolicy
프레임워크
DeclarativePolicy
프레임워크는 정책 검사를 수행하는 데 도움을 주고 EE(Enterprise Edition) 확장을 용이하게 하기 위해 설계되었습니다. app/policies
의 DSL 코드가 Ability.allowed?
에서 특정 행동이 주제에 대해 허용되는지 확인하는 데 사용됩니다.
정책은 주제의 클래스 이름을 기반으로 하므로, Ability.allowed?(user, :some_ability, project)
는 ProjectPolicy
를 생성하고 해당 권한을 확인합니다.
Ruby gem 소스는 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
에 대해 알 수 없습니다. 이를 통해 조건 수준에서만 캐시할 수 있습니다. 규칙은 블록의 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))
각 줄은 평가된 규칙을 나타냅니다. 주목해야 할 점이 몇 가지 있습니다:
-
-
기호는 규칙 블록이false
로 평가되었음을 나타냅니다.+
기호는 규칙 블록이true
로 평가되었음을 나타냅니다. -
괄호 안의 숫자는 점수를 나타냅니다.
-
줄의 마지막 부분(예:
@john : Issue/1
)은 해당 규칙의 사용자 이름과 주제를 보여줍니다.
여기에서 첫 번째 네 개의 규칙은 특정 사용자와 주제에 대해 false
로 평가되었음을 알 수 있습니다. 예를 들어, 마지막 줄에서는 사용자 john
이 Project/4
에서 Reporter 역할을 가지고 있기 때문에 규칙이 활성화되었음을 알 수 있습니다.
정책에 특정 능력이 허용되는지 문의할 때(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
예를 들어, 이는 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_broccoli
와 eats_what_is_given
과 같이) 다르게 명명할 수 있습니다. 이것은 호출 사이트가 얼마나 다형적인지에 따라 달라질 수 있습니다. 항상 Parent
또는 Child
로 정책을 확인하는 경우에는 적절한 능력 이름을 선택할 수 있습니다. 호출 사이트가 다형적인 경우에는 그렇게 할 수 없습니다.
정책 클래스 지정
주어진 주제에 대해 사용되는 정책을 재정의할 수도 있습니다:
class Foo
def self.declarative_policy_class
'SomeOtherPolicy'
end
end
이것은 일반적으로 계산된 FooPolicy
클래스가 아닌 SomeOtherPolicy
클래스를 사용하고 권한을 확인합니다.