⚡️ Pro Feature ⚡️ 해당 기능은GraphQL-Pro 에서만 가능하다.
Pundit Integration
GraphQL::Pro 에서는 GraphQL authorization 과 Pundit policies 의 통합을 사용할 수 있다.
귀찮게 왜 이렇게 해야되는가? GraphQL 타입 자체에 권한부여 코드를 넣을 수도 있지만 따로 권한부여 레이어를 작성하는 것은 몇가지 이점을 준다.
- authorization 코드가 GraphQL 에 포함되어 있지 않기 때문에 , 같은 로직을 앱의 GraphQL 이 아닌 부분(또는 레거시 코드)에도 사용 가능하다.
- authorization 로직은 독립되어 테스트 하는게 가능해지고, 그러므ㄹㅗ GraphQL 테스트는 그만큼 많은 가능성들을 테스트 하지 않아도 된다.
Getting Started
가장 최근의 젬을 사용하라. 다음과 같이 Gemfile에 추가.
# Pundit Integration을 위해:
gem "graphql-pro", ">=1.7.9"
# 리스트 스코핑을 위해:
gem "graphql", ">=1.8.7"
그리고 bundle install
수행
쿼리를 실행할 때마다 :current_user
를 context 에 포함해라.
context = {
current_user: current_user,
# ...
}
MySchema.execute(..., context: context)
앞으로 살펴볼 내용은 다음과 같다.
- Authorizing Objects 객체 권한부여
- Scoping Lists and Connections 리스트와 커넥션 스코핑
- Authorizing Fields 필드 권한부여
- Authorizing Arguments argument 권한부여
- Authorizing Mutations mutation 권한부여
반드시 사용자가 어떤 특정 타입의 객체들을 볼 수 있도록 하기 위해서 Pundit role 을 지정해줄 수 있다. 시작하기에 앞서 ObjectIntegration 를 베이스 객체 클래스에 추가해 보자.
# app/graphql/types/base_object.rb
class Types::BaseObject < GraphQL::Schema::Object
# Pundit integration 추가
include GraphQL::Pro::PunditIntegration::ObjectIntegration
# 디폴트로 staff를 필요로 함
pundit_role :staff
# 또는 디폴트로 no permission 을 할 수 있다.
# pundit_role nil
end
이제 GraphQL 객체를 읽으려고 하는 모든 사용자는 해당 객체 policy 의 #staff?
체크를 지나쳐야 한다.
그리고 나서, 각 하위 클래스는 부모 클래스의 설정을 오버라이드 할 수 있다. 예를 들어, 모든 사용자가 Query Root 를 읽을 수 있도록 하려면 다음과 같이 할 수 있다.
class Types::Query < Types::BaseObject
# 모든 사용자가 query root 를 볼 수 있도록 함
pundit_role nil
end
Policies and Methods
GraphQL 로 부터 돌려받은 각 객체에 대해, 통합은 이 것과 policy, method 를 일치시킨다. policy는 Pundit.policy!
를 사용하여 찾을 수 있는데 해당 객체의 클래스 이름을 사용하여 policy 를 찾을 수 있다.
그리고 난 후, GraphQL 은 해당 객체를 보는 것이 허용되는지 안되는지를 확인하기 위하여 policy 에 있는 메서드를 호출한다. 이 메서드는 다음과 같이 객체 클래스에 지정되어 있다.
class Types::Employee < Types::BaseObject
# 오직 보스에게만 직원의 객체를 보여거
# 또는 직원 자신의 정보만 보여준다
pundit_role :employer_or_self
# ...
end
위에서 #employer_or_self?
가 호출될 것이다.
Bypassing Policies
pundit 과 통합을 하기 위해서는pundit_role 이 있는 모든 객체에 대해서 상응하는 policy 클래스가 필요하다. pundit_role을 nil로 설정함으로써 해당 객체에 대해서 권한부여를 스킵할 수 있다.
class Types::PublicProfile < Types::BaseObject
# 모든 사람이 볼 수 있음
pundit_role nil
end
Handling Unauthorized Objects
만약 policy 메서드가 false를 리턴하면, 권한이 없는 객체는 Schema.unauthorized_object
에 전해지게 된다. ( 궈난이 없는 객체 핸들링은 Handling unauthorized objects 참고)
Scopes
Pundit 통합은 Pundit scopes 을 GraphQL-Ruby 의 list scoping 에 더하게 된다. 모든 리스트와 connection 은 scoped 될 것이다. 만약에 scope 이 없다면 쿼리는 필터되지 않은 데이터를 리턴하는 대신 크래쉬 하게 된다.
class BaseUnion < GraphQL::Schema::Union
include GraphQL::Pro::PunditIntegration::UnionIntegration
end
module BaseInterface
include GraphQL::Schema::Interface
include GraphQL::Pro::PunditIntegration::InterfaceIntegration
end
Pundit scope 은 데이터베이스 관계에는 가장 적합하지만 배열과는 잘 동작하지 않는다. 아래와 같이 만약 배열을 리턴하게 된다면 pundit 을 우회하도록 할 수 있다.
Bypassing scopes
# 모든 사람이 job posting을 볼 수 있도록 허용
field :job_postings, [Types::JobPosting], null: false,
scope: false
Authorizing Fields
필드 단위로 특정 검사를 할 수 있다. 우선, 베이스 필드 클래스에 include 하자.
# app/graphql/types/base_field.rb
class Types::BaseField < GraphQL::Schema::Field
# Pundit integration 추가
include GraphQL::Pro::PunditIntegration::FieldIntegration
# By default, don't require a role at field-level:
pundit_role nil
end
위의 base field class 를 base_object 와 base_interface 에도 추가해야 한다.
# app/graphql/types/base_object.rb
class Types::BaseObject < GraphQL::Schema::Object
field_class Types::BaseField
end
# app/graphql/types/base_interface.rb
module Types::BaseInterface
# ...
field_class Types::BaseField
end
그리고 pundit_role:
옵션을 필드에 추가하면 된다.
class Types::JobPosting < Types::BaseObject
# 로그인 한 사용자만 리스팅을 볼 수 있도록 함
pundit_role :signed_in
# 그러나 오직 staff 사용자만 appliants를 볼 수 있도록 함
field :applicants, [Types::User], null: true,
pundit_role: :staff
end
이 것은 부모 객체의 policy 로부터(여기서는 JobPostingPolicy) 해당 이름의 role 을 호출할 것이다(여기서는 #staff?
).
Authorizing Arguments
field-level 에서의 검사와 비슷하게, 특정 argument 에 대해서도 permission 를 요청할 수 있다. 이걸 하기 위해서는 base argument 클래스에 필요한 통합을 include 하면 된다.
class Types::BaseArgument < GraphQL::Schema::Argument
# 통합 추가, 디폴트로 어떠한 permission도 필요 없음
include GraphQL::Pro::PunditIntegration::ArgumentIntegration
pundit_role nil
end
위의 클래스를 base field 와 base input object 에 추가한다.
class Types::BaseField < GraphQL::Schema::Field
argument_class Types::BaseArgument
# 참고: base field 가 객체,인터페이스,뮤테이션과 잘 연결되어있는지를 보려면 "Authorizing Fields" 에서 확인
end
class Types::BaseInputObject < GraphQL::Schema::InputObject
argument_class Types::BaseArgument
end
이제 argument 는 pundit_role:
옵션을 받는다.
class Types::Company < Types::BaseObject
field :employees, Types::Employee.connection_type, null: true do
# 오직 admin 만이 employees 를 이메일로 필터링 할 수 있다
argument :email, String, required: false, pundit_role: :admin
end
end
해당 role 은 부모 객체의 policy 에서 호출될 것이다. 위의 경우에서는 CompanyPolicy#admin?
Authorizing Mutations
GraphQL mutation 에 pundit 통합을 이용하여 권한부여 하는 방법에는 여러가지가 있다.
- mutation-level roles 추가하기
- objects loaded by ID 에 검사 실행하기
Setup
MutationIntegration
를 base mutation 에 추가한다.
class Mutations::BaseMutation < GraphQL::Schema::Mutation
include GraphQL::Pro::PunditIntegration::MutationIntegration
# argument-level 권한부여 기능을 사용하기 위함:
argument_class Types::BaseArgument
end
디폴트 role 을 설정하기 위해 BaseMutationPayload
를 사용할 수도 있다.
class Types::BaseMutationPayload < Types::BaseObject
# 만약 `BaseObject` 이 permission 을 필요로 한다면 mutatiohn 결과는 무시한다.
# mutation을 실행한 사람은 해당 mutation의 결과를 볼 수 있다고 가정한다.
pundit_role nil
end
위의 BaseMutationPayload 를 base mutation에 연결하자.
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
object_class Types::BaseMutationPayload
end
Mutation-level roles
각 mutation 은 class-level pundit_role 을 가질 수 있는데 이는 객체를 로딩하거나 resolve 하기 전에 검사될 것이다.
class Mutations::PromoteEmployee < Mutations::BaseMutation
pundit_role :admin
end
위의 예시에서는 PromoteEmployeePolicy#admin?
이 mutation을 실행하기 전에 검사될 것이다.
Custom Policy Class
디폴트로 pundit 은 policy 를 찾기 위해서 mutation 의 클래스 이름을 이용한다. self.policy_class
를 mutation 에 정의함으로써 이를 오버라이딩 하는 것이 가능하다.
class Mutations::PromoteEmployee < Mutations::BaseMutation
def self.policy_class
::UserPolicy
end
pundit_role :admin
end
이제부터 mutation 은 PromoteEmployeePolicy#admin? 대신에 UserPolicy#admin?
를 확인하게 된다.
또 다른 좋은 방법은 각 muation 마다 policy 를 갖는 것이다. mutation 내에 있는 클래스를 찾기 위해서 self.policy_class를 구현할 수 있다.
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
def self.policy_class
# nested 된 `Policy` 상수를 검색:
self.const_get(:Policy)
end
end
그렇다면 각 mutation 은 각자의 policy 를 정의할 수 있게 된다.
class Mutations::PromoteEmployee < Mutations::BaseMutation
# 이것은 위에서 정의한`BaseMutation.policy_class`에서 찾을 수 있다:
class Policy
# ...
end
pundit_role :admin
end
이제부터 Mutations::PromoteEmployee::Policy#admin
이 mutation 을 실행하기 이전에 검사될 것이다.
loads: 옵션을 사용하여 mutation 은 자동적으로 ID 를 이용하여 객체를 로드하거나 권한부여를 할 수 있다.
일반적인 객체 permission 에서 나아가, pundit_role:
옵션을 사용해서 특정 mutation input 에 추가적인 role 을 추가할 수 있다.
class Mutations::FireEmployee < Mutations::BaseMutation
argument :employee_id, ID, required: true,
loads: Types::Employee,
pundit_role: :supervisor,
end
위의 경우에서는 EmployeePolicy#supervisor?
메서드가 true 를 리턴하지 않는다면 mutation 은 중단될 것이다.
Unauthorized Mutations
기본적으로 mutation 에서의 권한부여 실패는 Ruby 예외를 발생시킨다. base mutation 파일에서 #unauthorized_by_pundit(owner, value)
를 구현함으로써 이를 커스터 마이징 할 수 있다.
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
def unauthorized_by_pundit(owner, value)
# 에러 없음. 그냥 nil 리턴
nil
end
end
이 메서드는 다음과 함께 호출될 수 있다.
owner
: theGraphQL::Schema::Argument
or mutation class whose role was not satisfiedvalue
: the object which didn’t pass forcontext[:current_user]
이 것은 mutation 메서드이기 때문에 위의 메서드에서 context에 접근할 수 있다.
위 메서드가 반환하는 게 무엇이던지 간에 mutation 의 초기 리턴값으로 취급될 것이다. 그러므로 예를 들어 에러를 데이터로써 반환하는 것이 가능하다.
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
field :errors, [String], null: true
def unauthorized_by_pundit(owner, value)
# 에러를 데이터로써 리턴
{ errors: ["Missing required permission: #{owner.pundit_role}, can't access #{value.inspect}"] }
end
end
* 해당 글은 번역기 돌리다가 크롬 번역기 말도 안되는 해석에 지친 본인이 나중에 참고할 의도로 대충대충 발로 해석한 것이니 참고용으로만 사용하시길 바랍니다.
* 출처: http://graphql-ruby.org/authorization/pundit_integration.html
'배움의 즐거움 > 프로그래밍' 카테고리의 다른 글
(31) Graphql-ruby - 필드(fields) 의 모든 것 (0) | 2019.01.01 |
---|---|
(30) Graphql-ruby - 범위설정(scoping) (0) | 2019.01.01 |
(28) Graphql-ruby - Authorization 의 모든 것 (0) | 2019.01.01 |
(27) Graphql-ruby - Non-Null 타입 (0) | 2019.01.01 |
(26) Graphql-ruby - 리스트 (0) | 2019.01.01 |