본문 바로가기

배움의 즐거움/프로그래밍

(29) Graphql-ruby - Pundit Integration

반응형




⚡️ 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


반드시 사용자가 어떤 특정 타입의 객체들을 볼 수 있도록 하기 위해서 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


scope: false 을 통해 scope 을 설정하지 않는 것도 가능
# 모든 사람이 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 통합을 이용하여 권한부여 하는 방법에는 여러가지가 있다.


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 을 실행하기 이전에 검사될 것이다.


Authorizing Loaded Objects


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: the GraphQL::Schema::Argument or mutation class whose role was not satisfied

  • value: the object which didn’t pass for context[: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


반응형