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_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) # 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를 참조하세요.
이 예에서 DiscussionType
은 authorize :read_note
를 명시합니다. Discussion
은 NoteType
유형의 여러 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
에서 논의를 승인합니다. - 하나의 노트를 읽거나 모든 노트를 읽는 권한은 논의에 대해 동일합니다.
- 노트를 읽거나 이모지를 읽는 권한은 이 경우 동등합니다.