GraphQL 인증

인가는 다음 위치에 적용할 수 있습니다.

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

추상 유형(인터페이스 및 유니언)에 대해 인가를 지정할 수 없습니다. 추상 유형은 멤버 유형으로 위임합니다. 기본 내장 스칼라(예: 정수)에는 인가가 없습니다.

저희의 인가 시스템은 애플리케이션의 다른 부분에서와 마찬가지로 동일한 DeclarativePolicy 시스템을 사용합니다.

  • 단일 값(예: Query.project와 같은)에 대해서는 현재 인증된 사용자가 인가에 실패하면 해당 필드가 null로 해석됩니다.
  • 컬렉션(예: Project.issues와 같은)에 대해서는 사용자의 인가 확인에 실패한 객체를 필터링하여 컬렉션이 제외됩니다. 이러한 필터링 프로세스(레드액션으로도 알려짐)는 페이지네이션 이후에 발생하므로 레드액션된 객체가 제거되어 요청된 페이지 크기보다 작은 페이지가 될 수 있습니다.

또한, 뮤테이션에서 리소스에 대한 인가를 참조하십시오.

참고: 현재 인증된 사용자가 볼 수 있는 것만로드 하는 것이 좋은 실천 방법입니다. 이로인해 데이터베이스 쿼리와 불필요한 인가 확인을 최소화합니다. 또한 민감한 리소스의 존재를 노출할 수 있는 짧은 페이지와 같은 상황을 피할 수 있습니다.

여기서 이에 관한 모든 인가 체계의 예제를 보려면 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) # 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)

유형 및 필드 인가의 결합

인가는 누적됩니다. 다시 말해 현재 인증된 사용자는 필드뿐만 아니라 필드의 유형에서 인가 요구 사항을 충족해야 할 수 있습니다.

다음 간소화된 예제에서 현재 인증된 사용자는 사용자와 문제의 작성자를 볼 수 있도록 UserType의 객체 인가 및 IssueType.author의 필드 인가의 두 요소를 모두 충족해야 합니다.

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를 사용하여 해결되며, 이 resolver가 해결된 객체의 권한 확인을 처리합니다.

특히 해당 필드가 객체 컬렉션을 해결하는 경우 유형 수준의 권한 확인을 건너뛰고 싶을 수 있습니다. GraphQL 쿼리에 따라 이러한 겹치는 권한 확인은 상당한 오버헤드를 초래할 수 있습니다.

이러한 경우, 특정 필드의 skip_type_authorization를 통해 유형 수준의 스킵해야 할 능력을 지정할 수 있습니다. 이 옵션은 모든 하위 필드로도 계속 적용됩니다.

실제 예시는 다음을 참조하세요. field :discussions, Types::Notes::DiscussionType.

해당 예시에서는 DiscussionTypeauthorize :read_note를 지정하고 있습니다. Discussion은 여러 개의 NoteType 유형의 notes로 구성되어 있으며 NoteType 또한 authorize: :read_note를 지정합니다. 일부 노트는 시스템 노트이며 SystemNoteMetadataType 유형의 특정 메타데이터를 가질 수 있습니다. SystemNoteMetadataTypeauthorize: :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 = 100개의 승인 호출이 발생합니다. 마지막으로 각 토론의 첫 번째 노트에 대해 10개의 호출로 이모지를 승인합니다. 따라서 총 130개의 승인 호출이 발생합니다:

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

이를 discussions 필드에 skip_type_authorization를 지정하여 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에서 토론을 승인함 - 토론의 하나 또는 모든 노트를 읽는 데 필요한 권한은 동일함 - 노트를 읽거나 이모지를 읽는 데 필요한 권한이 동등함