본문 바로가기

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

(28) Graphql-ruby - Authorization 의 모든 것

반응형





Overview


다음은 GraphQL 권한부여에 대한 개념적인 접근과 내장된 권한부여 프레임워크에 대한 설명이다. 프레임워크의 각 부분은 각각의 가이드에 설명되어 있다.


Authorization: GraphQL vs REST


REST API 에서는 일반적인 권한 부여 패턴은 꽤 단순하다. 요청된 액션을 시행하기 이전에 서버는 현재 클라이언트가 해당 액션에 대한 필요한 권한이 있는지 확인한다.
class PostsController < ApiController
  def create
    # 우선 클라이언트의 허가 레벨 체크
    if current_user.can?(:create_posts)
      # 만약 사용자가 허용된다면 액션을 수행한다
      post = Post.create(params)
      render json: post
    else
      # 그렇지 않다면 에러를 일으킴
      render nothing: true, status: 403
    end
  end
end


그러나 이 요청별로 하는 방법은 GraphQL 과 잘 매핑되지 않는데, 왜냐하면 컨트롤러는 오직 하나만 있고 들어오는 요청들은 각기 다르기 때문이다. 문제점을 한번 봐보자.

class GraphqlController < ApplicationController
  def execute
    # `query_str` 쿼리를 위해서는 어떠한 permission 이 필요한가?
    # 이것은 string에 달려있다. 그러므로 이 레벨에서는 이를 일반화 할 수 없다
    if current_user.can?(:"???")
      MySchema.execute(query_str, context: ctx, variables: variables)
    end
  end
end

그러므로 graphQL API 에서는 다른 방법이 필요하다.


mutation 에서는, 각 mutation 이 API 요청 자체와 같다는 것을 기억해야 한다. 예를 들어, 위의 Posts#creat는 GraphQL 에서의createPost(...) mutation 과 맵핑할 수 있다. 그러므로 각 mutation 은 각자의 권한에 맞게 권한부여가 되어야 한다.


query 에서는, 각각의 객체를 REST API 에서의 GET 요청이라고 보면 된다. 그러므로 각 객체는 각자의 권한에 맞게 권한 부여가 되어야 한다.


이러한 방법을 적용함으로써 GraphQL 의 쿼리의 모든 파트는 적절히 수행되기 전에 권한 부여가 될 수 있다.또한 다른 코드의 유닛이 각자에 맞추어 권한 부여가 되기 떄문에, 서버가 전에 본 적 없는 새로운 쿼리라 할 지라도 들어오는 쿼리가 적절히 권한부여될 것이라는 것을 확신할 수 있다.


What About Authentication?


참고로

  • 인증(Authentication) 은 사용자 이름이나 비밀번호를 수락하거나,  session[:current_user_id] 으로부터 User를 데이터베이스에서 찾는 것과 같이 사용자가 현재 요청하는 것을 결정하는 과정이다.
  • 권한부여(Authorization)  는 현재 사용자가 어떠한 행동을 하는데 권한이 있는지를 확인하는 과정이다. 예를 들어 사용자가 admin 인지를 확인하거나 데이터베이스로부터 권한 그룹을 검색하는 것이 있다.


일반적으로, 인증은 GraphQL 에서 다뤄지지 않는다. 대신 너의 컨트롤러가 HTTP 요청에 기반하여 현재 사용자를 가져오고 해당 정보를 GraphQL 쿼리에 제공한다. 예를 들어

class GraphqlController < ApplicationController
  def execute
    # HTTP 요청으로부터 현재 사용자를 어떻게든 가져옴
    current_user = get_logged_in_user(request)
    # 해당 쿼리를 진행하는 동안 현재 사용자를 `context` 에 제공
    context = { current_user: current_user }
    MySchema.execute(query_str, context: context, ...)
  end
end

HTTP 핸들러가 현재 사용자를 로드한 후에, 이를 GraphQL 코드에 있는 context[:current_user] 를 통해 사용할 수 있다.


Authorization in Your Business Logic


GraphQL  구체적인 권한부여에 대해서 설명하기 전에, application-level 권한 부여의 장점에 대해서 생각해보자. (여기서 더 자세히 볼 수 있다.GraphQL.org post) 예를 들어, 아래는 권한 부여가 GraphQL API 에 믹스된 것이다.
field :posts, [Types::Post], null: false

def posts
  # GraphQL 필드에 대해 권한체크 수행
  if context[:current_user].admin?
    Post.all
  else
    Post.published
  end
end

이것의 단점은 Types::Post가 다른 context 에서 요청되었을 때, 같은 권한 체크가 이루어지지 않을 수 있다는 점이다. 추가적으로 권한부여 코드가 GraphQL API와 엮여있기 때문에 이를 테스트할 수 있는 유일한 방법은 GraphQL 을 통해서인데, 이는 테스트에 복잡성을 더하게 된다.


대안으로, 해당 권한 체크를 Post클래스로 옮길 수 있다.

class Post < ActiveRecord::Base
  # `user` 가 볼 수 있는 포스트를 리턴함
  def self.posts_for(user)
    if user.admin?
      self.all
    else
      self.published
    end
  end
end


그리고 이를 GraphQL 코드에 적용한다.

field :posts, [Types::Post], null: false

def posts
  # 해당 사용자가 볼 수 있는 포스트를 가져온다
  Post.posts_for(context[:current_user])
end

이 경우에서 Post.posts_for(user) 부분은 GraphQL 과는 독립적으로 테스트가 가능하다. 그리고 나면 GraphQL 테스트에서 덜 걱정해도 된다. 추가로, 앱의 다른 부분에서  Post.posts_for(user) 를 사용할 수 있다.


GraphQL-Ruby’s Authorization Framework


어플리케이션 단에서 권한 부여를 하는 것의 장점들에도 불구하고, 위에서 말한 것과 같이 API 단에서 권한부여를 해야 하는 다른 이유들이 있다. 

  • API 단이 안전하다는 더 큰 확신을 준다
  • 이를 실행하기 전에 API 요청에 대해 권한체크를 한다.(아래 “visibility” 를 보라)
  • 권한부여가 내장되어 있지 않은 코드와 통합할 수 있다



이를 수행하기 위해서는 GraphQL-Ruby 의 권한부여 프레임워크를 사용하면 된다. 프레임워크는 3가지 레벨이 있는데 이는 모두 이 가이드에 설명되어 있다.

  • Visibility 전체 권한이 없는 사용자에 대해서 GraphQL 스키마의 일부분을 숨김
  • Accessibility 사용자가 필요한 권한이 없다면 GraphQL 스키마의 일부분에 접근하는 쿼리를 막음
  • Authorization 사용자가 객체에 접근 권한이 있는지 확인하기 위해서 실행하는 동안 해당 객체를 체크함


Visibility


GraphQL-Ruby 에서는 일부 사용자로부터 스키마의 일부분을 보여주지 않도록 하는 것이 가능하다. 이것은 정확히 말해서는 GraphQL 스펙은 아니지만 대략적으로 경계에 있다.


Visibility 는 다음과 같은 경우에서 사용될 수 있다.

  • 어드민이 아닌 사용자에게 어드민만 가능한 부분을 보여주지 않고 싶을 때

  • 새로운 기능을 개발중인데 일부 사용자에게만 점차적으로 공개하고 싶을 때


Hiding Parts of the Schema


visible? 메서드를 재구현함으로써 스키마의 일부분을 보여주도록 커스터마이징 할 수 있다.
  • 타입 클래스는.visible?(context) 클래스 메서드를 가지고 있다.
  • 필드와 arguments 는 #visible?(context) 인스턴스 메서드를 가지고 있다.
  • Enum 값은  #visible?(context) 인스턴스 메서드를 가지고 있다.
  • Mutation 클래스는.visible?(context)  클래스 메서드를 가지고 있다.

이 메서드들은 쿼리 context 와 함께 호출되는데 context:로써 전달된 해쉬를 기반으로 한다. 만약 메서드가 false 를 리턴하면 스키마의 해당 멤버가 전체 쿼리에 대해 존재하지 않은 것 처럼 처리된다. 즉:
  • introspection 에서 해당 멤버는 결과값에 절대로 포함되지 않는다.
  • 일반적인 쿼리에서 쿼리가 해당 멤버를 언급한다면, 해당 멤버는 존재하지 않는 것으로 나오기 때문에 유효성 에러를 리턴하게 된다.


For Example


만약 너가 잠시 동안은 비밀로 있어야 하는 새로운 기능을 작업하고 있다고 보자. 해당 타입에  .visible? 를 구현하면 된다.

class Types::SecretFeature < Types::BaseObject
  def self.visible?(context)
    # secret_feature 이 있는 사용자에게만 보여줌
    super && context[:viewer].feature_enabled?(:secret_feature)
  end
end

(기본적인 behavior 을 상속받기 위해서 super 를 호출하라)


이제, 다음 GraphQL은 유효성 에러를 리턴할 것이다.

  • SecretFeature를 리턴하는 필드, 예를 들면 query { findSecretFeature { ... } }
  • SecretFeatureFragment 들, 예를 들면 Fragment SF on SecretFeature


그리고 introspection 에서는:

  • __schema { types { ... } } 는 SecretFeature 를 포함하지 않을 것이다.
  • __type(name: "SecretFeature") 는  nil 을 리턴할 것이다.
  • SecretFeature를 포함한 모든 interfaces 또는 unions 은 이를 포함하지 않을 것이다.
  • SecretFeature 를 리턴하는 모든 필드는 introspection에서 제외될 것이다. 


Accessibility


GraphQL-Ruby 에서는 들어오는 쿼리를 점검할 수 있고 만약 쿼리가 허가받지 않는 스키마의 부분에 접근하려 한다면 커스텀 에러를 리턴할 수 있다.

이것은 허용되지 않은 스키마의 부분이 존재하지 않는 것처럼 다뤄지는 visibility 와는 약간 다르다. 이것은 또한 실행 전에 체크하는 것이 아니라 실행 중에 체크하는 authorization(권한부여)와도 다르다.

(즉, accessiblity는 존재하는 걸로 나오지만 에러를 리턴하고, 실행 전에 체크된다?)


Preventing Access


스키마의 일부 멤버에 접근하는 것을 막기 위해 .accessible?(context) 메서드를 오버라이드 할 수 있다.

  • Type 과 mutation 클래스는.accessible?(context) 클래스 메서드를 가지고 있다.
  • Arguments 와 fields .accessible?(context) 인스턴스 메서드를 가지고 있다.

이 메서드들은 쿼리 context 와 함께 호출되는데 이는 context:로써 전달되는 해쉬를 기반으로 한다.

Whenever that method is implemented to return falsethe currently-checked field will be collected as inaccessible. For example:

class BaseField < GraphQL::Schema::Field
  def initialize(preview:, **kwargs, &block)
    @preview = preview
    super(**kwargs, &block)
  end

  # 만약 해당 필드가 preview: true 라면, 해당 사용자가 can_preview? 가 true 일때만 이 필드를 보여줌
  def accessible?(context)
    if @preview && !context[:viewer].can_preview?
      false
    else
      super
    end
  end
end

이제 field(..., preview: true) 는 모든 사용자들에게 보여지지만, .can_preview? 가 true 인 사용자만 접근 가능하다.


Adding an Error


디폴트로 GraphQL-Ruby는 .accessible? 체크가 false 를 리턴한다면 간단한 에러를 클라이언트에게 리턴한다. Schema.inaccessible_fields 를 오버라이딩 함으로써 이를 커스터마이징 하는 것은 가능하다.

class MySchema < GraphQL::Schema
  # `GraphQL::Field` 클래스에 커스텀 `permission_level` 세팅이 있다면
  # 여기서 그것에 접근할 수 있다.
  def self.inaccessible_fields(error)
    required_permissions = error.fields.map(&:permission_level).uniq
    # 커스텀 에러 리턴
    GraphQL::AnalysisError.new("You need certain permissions: #{required_permissions.join(", ")}")
  end
end

그리고 나면 너의 커스텀 에러는 디폴트 에러를 대신하여 응답에 추가될 것이다.


Authorization


쿼리가 실행되는 동안, 현재 사용자가 해당 객체와 상호작용할 수 있는 권한이 있는지 각 객체마다 체크할 수 있다. 만약 사용자가 권한이 없다면 에러로 핸들링이 가능하다.


Adding Authorization Checks


스키마 멤버들은 실행 중에 호출될 .authorized?(value, context) 메서드를 가지고 있다.

  • 타입과 and mutation 클래스들은  .authorized?(value, context) 클래스 메서드를 가지고 있다.
  • 필드와argument는 #authorized?(value, context) 인스턴스 메서드를 가지고 있다.


이 메서드들은 다음의 것들과 함께 호출될 수 있다.

  • object: 필드로부터 리턴된 객체
  • context: context 로써 전달된 context 해쉬에 기반한 쿼리 context


만약 해당 메서드가 false 를 리턴한다면 쿼리는 중단될 것이다.

class Types::Friendship < Types::BaseObject
  # You can only see the details on a `Friendship`
  # if you're one of the people involved in it.
def self.authorized?(object, context) super && (object.to_friend == context[:viewer] || object.from_friend == context[:viewer]) end end

(디폴트 체크를 하기 위해 항상 super 을 호출하여라)


이제 타입 Friendship 의 객체가 클라이언트에게 리턴될 때마다, 이 것은 우선 .authorized? 메서드를 지나쳐 갈 것이다. 만약 메서드가 false 를 리턴한다면, 필드는 원래 객체가 아닌 nil 을 리턴할 것이며, 너는 아마도 에러와 함께 해당 케이스를 핸들링 해야할 것이다.(아래 참고)


Handling Unauthorized Objects


디폴트로 GraphQL-Ruby 는 마치 그들이 존재하지 않는 것 처럼 권한이 없는 객체를 nil 로 대체한다. 너는 스키마 클래스에서 Schema.unauthorized_object 를   오버라이딩 함으로써 커스터마이징 할 수 있다.

class MySchema < GraphQL::Schema
  #`authorized?` 가 false 를 리턴하는 경우를 대비해 이 hook를 오버라이드
  def self.unauthorized_object(error)
    # nil을 리턴하는 대신 top-level 에러를 응답에 추가:
    raise GraphQL::ExecutionError, "An object of type #{error.type.graphql_name} was hidden due to permissions"
  end
end

이제부터 커스텀 hook 이 디폴트로 되어있는 것을 대신하여 호출될 것이다.


만약 .unauthorized_object 가 non-nil 객체를 리턴한다면(그리고 에러가 발생하지 않는다면), 해당 객체는 권한이 없는 객체를 대신하여 사용될 것이다.


* 해당 글은 번역기 돌리다가 크롬 번역기 말도 안되는 해석에 지친 본인이 나중에 참고할 의도로 대충대충 발로 해석한 것이니 참고용으로만 사용하시길 바랍니다.

* 출처: http://graphql-ruby.org/authorization/overview.html

* 출처: http://graphql-ruby.org/authorization/visibility.html

* 출처: http://graphql-ruby.org/authorization/accessibility.html

* 출처: http://graphql-ruby.org/authorization/authorization.html




반응형