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_project
및 another_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.
해당 예시에서는 DiscussionType
에 authorize :read_note
를 지정하고 있습니다. Discussion
은 여러 개의 NoteType
유형의 notes
로 구성되어 있으며 NoteType
또한 authorize: :read_note
를 지정합니다.
일부 노트는 시스템 노트이며 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 = 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
에서 토론을 승인함
- 토론의 하나 또는 모든 노트를 읽는 데 필요한 권한은 동일함
- 노트를 읽거나 이모지를 읽는 데 필요한 권한이 동등함