GraphQL 권한 부여

권한은 다음 장소에 적용될 수 있습니다:

  • 유형:
    • 객체 (::Types::BaseObject에서 파생된 모든 클래스)
    • 열거형 (::Types::BaseEnum에서 파생된 모든 클래스)
  • 리졸버:
    • 필드 리졸버 (::Types::BaseResolver에서 파생된 모든 클래스)
    • 뮤테이션 (::Types::BaseMutation에서 파생된 모든 클래스)
  • 필드 (field DSL 메소드를 사용하여 선언된 모든 필드)

추상 유형(인터페이스 및 유니온)에 대한 권한을 지정할 수 없습니다. 추상 유형은 그들의 멤버 유형에 위임합니다.

기본 내장 스칼라(예: 정수)는 권한이 없습니다.

우리의 권한 시스템은 애플리케이션의 나머지 부분과 동일한 DeclarativePolicy 시스템을 사용합니다.

  • 단일 값(예: Query.project)의 경우, 현재 인증된 사용자가 권한을 통과하지 못하면 필드는 null로 해결됩니다.

  • 컬렉션(예: Project.issues)의 경우, 컬렉션은 사용자의 권한 확인이 실패한 객체를 제외하도록 필터링됩니다. 이 필터링 과정(또는 _레닥션_이라고도 함)은 페이지네이션 후에 이루어지므로, 익명화된 객체가 제거되어 요청된 페이지 크기보다 작은 페이지가 발생할 수 있습니다.

뮤테이션에서 리소스를 권한 부여하는 방법도 참조하세요 authorizing resources in a mutation.

참고:

모범 사례는 현재 인증된 사용자가 볼 수 있는 것만 로드하는 것입니다.

기존의 파인더를 사용하여 권한 부여에 의존하지 않고 기록을 필터링하는 것이 좋습니다.

이렇게 하면 데이터베이스 쿼리와 로드된 기록에 대한 불필요한 권한 검사 최소화 할 수 있습니다.

또한 기밀 리소스의 존재를 드러낼 수 있는 짧은 페이지와 같은 상황을 피할 수 있습니다.

여기서 논의된 모든 권한 제도의 예시는 authorization_spec.rb에서 확인할 수 있습니다.

유형 권한 부여

authorize 메소드에 능력을 전달하여 유형을 권한 부여합니다.

같은 유형의 모든 필드는 현재 인증된 사용자가 필요한 능력을 가지고 있는지 확인하여 권한을 부여받습니다.

예를 들어, 다음 권한 부여는 현재 인증된 사용자가 그들이 read_project 능력을 가진 프로젝트만 볼 수 있도록 보장합니다(프로젝트가 Types::ProjectType을 사용하는 필드에서 반환되는 한):

module Types
  class ProjectType < BaseObject
    authorize :read_project
  end
end

또한 여러 능력에 대해 권한을 부여할 수 있으며, 이 경우 모든 능력 확인이 통과해야 합니다.

예를 들어, 다음 권한 부여는 현재 인증된 사용자가 프로젝트를 보려면 read_projectanother_ability 능력이 필요하다는 것을 보장합니다:

module Types
  class ProjectType < BaseObject
    authorize [:read_project, :another_ability]
  end
end

리졸버 권한 부여

리졸버는 자신만의 권한 부여를 가질 수 있으며, 이는 부모 객체나 해결된 값에 적용될 수 있습니다.

부모에 대한 권한을 부여하는 리졸버의 예는 Resolvers::BoardListsResolver입니다. 이 리졸버는 부모가 :read_list를 만족해야 실행됩니다.

해결된 리소스에 대한 권한을 부여하는 예는 Resolvers::Ci::ConfigResolver로, 이 리졸버는 해결된 값이 :read_pipeline을 만족해야 합니다.

부모에 대한 권한을 부여하려면, 리졸버는 authorizes_object!로 이를 선언하여 _옵트인_해야 합니다(이는 초기 기본값이 아니었기 때문입니다):

module Resolvers
  class MyResolver < BaseResolver
    authorizes_object!

    authorize :some_permission
  end
end

해결된 값에 대한 권한을 부여하려면, 리졸버는 일반적으로 #authorized_find!(**args)를 사용하여 어떤 시점에 권한 부여를 적용해야 합니다:

module Resolvers
  class MyResolver < BaseResolver
    authorize :some_permission

    def resolve(**args)
      authorized_find!(**args) # calls find_object
    end

    def find_object(id:)
      MyThing.find(id)
    end
  end
end

두 가지 접근 방식 중 객체를 권한 부여하는 것이 더 효율적인데, 이는 불필요한 쿼리를 피하는 데 도움이 되기 때문입니다.

필드 권한 부여

필드는 authorize 옵션으로 권한 부여할 수 있습니다.

필드 권한 부여는 현재 객체에 대해 확인되며,

권한 부여는 해결 이전에 발생하므로

필드는 해결된 리소스에 접근할 수 없습니다. 필드에 권한 부여 검사를 적용해야 하는 경우, 리졸버에 권한 부여를 추가하거나 이상적으로는 타입에 권한을 추가하는 것이 좋습니다.

예를 들어, 다음 권한 부여는 인증된 사용자가 secretName 필드를 보기 위해 프로젝트에 관리자 수준의 접근 권한이 있어야 함을 보장합니다:

module Types
  class ProjectType < BaseObject
    field :secret_name, ::GraphQL::Types::String, null: true, authorize: :owner_access
  end
end

이 예제에서는 필드 권한 부여(Ability.allowed?(current_user, :read_transactions, bank_account))를 사용하여

더 비싼 쿼리를 피합니다:

module Types
  class BankAccountType < BaseObject
    field :transactions, ::Types::TransactionType.connection_type, null: true,
      authorize: :read_transactions
  end
end

필드 권한 부여는 다음과 같은 경우에 권장됩니다:

  • 다른 필드에 대해 서로 다른 접근 제어 수준을 가져야 하는 스칼라 필드(문자열, 불리언 또는 숫자)
  • 부모에 대해 접근 검사를 적용하여 필드 해제를 저장하고 각 해결된 객체에 대한 개별 정책 검사 피할 수 있는 객체 및 컬렉션 필드

필드 권한 부여는 객체 수준 검사를 대체하지 않으며, 객체가 부모 프로젝트의 접근 수준과 정확히 일치하지 않는 한 대체되지 않습니다. 예를 들어, 문제는

부모의 접근 수준에 독립적으로 기밀로 만들 수 있습니다. 따라서 Project.issue에 대해 필드 권한 부여를 사용해서는 안 됩니다.

필드를 여러 능력에 대해 권한 부여할 수도 있습니다. 단일 값 대신 배열로 능력을 전달하면 됩니다:

module Types
  class MyType < BaseObject
    field :hidden_field, ::GraphQL::Types::Int,
      null: true,
      authorize: [:owner_access, :another_ability]
  end
end

MyType.hiddenField에 대한 필드 권한 부여는 다음과 같은 테스트를 포함합니다:

Ability.allowed?(current_user, :owner_access, object_of_my_type) &&
    Ability.allowed?(current_user, :another_ability, object_of_my_type)

타입 및 필드 권한 부여 함께

권한 부여는 누적됩니다. 즉, 현재 인증된 사용자는 필드와 필드의 타입 모두에서 권한 부여 요구 사항을 충족해야 할 수 있습니다.

다음의 단순화된 예에서 현재 인증된 사용자는 사용자에 대한 first_permission

문제에 대한 second_permission을 모두 필요로 하며 문제의 저자를 볼 수 있습니다.

class UserType
  authorize :first_permission
end
class IssueType
  field :author, UserType, authorize: :second_permission
end

UserType의 객체 권한 부여와 IssueType.author의 필드 권한 부여를 결합하면,

다음과 같은 테스트를 포함합니다:

Ability.allowed?(current_user, :second_permission, issue) &&
  Ability.allowed?(current_user, :first_permission, issue.author)

주어진 필드에 대한 타입 권한 부여 건너뛰기

일부 시나리오에서는 주어진 필드가 전용 resolver로 해결되고 해결자는 해결된 객체의 권한 부여를 확인합니다.

이러한 경우, 특히 필드가 객체의 컬렉션을 해결하는 경우, 타입 수준 권한 부여를 건너뛰는 것이 좋습니다. GraphQL 쿼리에 따라 이러한 중복 권한 부여 검사가 있으면 상당한 오버헤드가 추가될 수 있습니다.

이러한 상황에서는 주어진 필드의 skip_type_authorization을 통해 Type 수준에서 건너뛸 수 있는 능력을 명시할 수 있습니다. 이 옵션은 모든 하위 필드에 cascading 됩니다.

실제 예제를 보려면 field :discussions, Types::Notes::DiscussionType를 참조하세요.

이 예에서 DiscussionTypeauthorize :read_note를 명시합니다. DiscussionNoteType 유형의 여러 notes로 구성되어 있으며 NoteType 역시 authorize: :read_note를 명시합니다.

notes 중 일부는 시스템 노트일 수 있으며 SystemNoteMetadataType 유형의 특정 메타데이터를 가질 수 있습니다. SystemNoteMetadataType 또한 authorize: :read_note를 명시합니다. 각 노트에는 이 경우 read_note와 동등한 read_emoji라는 이모지가 있을 수 있습니다.

GraphQL 예제를 나타내기 위해 다음과 같은 유형을 가질 수 있습니다:

class SomeType < BaseObject
  field :discussions, Types::Notes::DiscussionType.connection_type, null: true, resolver: SomeResolver
end

class DiscussionType < BaseObject
  authorize :read_note

  field :notes, Types::Notes::NoteType.connection_type, null: true
end

class NoteType < BaseObject
  authorize :read_note

  field :system_note_metadata, SystemNoteMetadataType
  field :award_emoji, AwardEmojiType
end

class SystemNoteMetadataType < BaseObject
  authorize :read_note
end

class AwardEmojiType < BaseObject
  authorize :read_emoji
end

그리고 다음과 같은 쿼리:

query {
  someType(identified: ID) {
    discussions {
      nodes {
        notes {
          nodes {
            award_emoji {
              name
            }
          }
        }
      }
    }
  }
}

SomeType의 루트 객체가 10개의 논의를 가진다고 가정해 봅시다. 각 10개의 논의는 10개의 노트를 가지고 있습니다. 각 논의의 첫 번째 노트는 하나의 이모지를 가지고 있습니다.

이 경우, SomeResolver에서 논의를 승인하므로 10개의 권한 부여 호출이 발생합니다.

그 후, 각 논의를 DiscussionType으로 표현할 때, 다시 10개의 호출로 각 논의 객체를 승인합니다. 이러한 특정 호출은 resolver 권한 부여 동안 요청 저장소에 캐시되어 있기 때문에 괜찮을 수 있습니다.

다음으로, 이러한 10개의 논의에 대해 각 노트를 승인하므로 10*10 = 100개의 권한 부여 호출이 발생합니다. 마지막으로, 각 논의의 첫 번째 노트에 대해 하나의 이모지를 승인하므로 또 다른 10개의 호출이 발생합니다. 따라서 총 130개의 권한 부여 호출이 발생합니다:

  • resolver에서 승인된 10개의 논의
  • DiscussionType을 통해 승인된 10개(캐시된) 논의
  • NoteType을 통해 승인된 100개의 노트
  • EmojiType을 통해 승인된 10개의 이모지

discussions 필드에서 skip_type_authorization을 지정하여 이 130개 호출을 10개 호출로 줄일 수 있습니다.

이 경우 SomeType 정의는 다음과 같이 변경됩니다:

class SomeType < BaseObject
  field :discussions, Types::Notes::DiscussionType.connection_type, null: true, resolver: SomeResolver,
        skip_type_authorization: [:read_note, :read_emoji]
end

참고: skip_type_authorization을 통해 권한 부여 호출을 최적화할 수 있는 이유는 다음과 같습니다:

  • 우리는 이미 SomeResolver에서 논의를 승인합니다.
  • 하나의 노트를 읽거나 모든 노트를 읽는 권한은 논의에 대해 동일합니다.
  • 노트를 읽거나 이모지를 읽는 권한은 이 경우 동등합니다.