GraphQL 授权
授权可以应用在以下位置:
- 类型:
- 对象(所有继承自
::Types::BaseObject的类) - 枚举(所有继承自
::Types::BaseEnum的类)
- 对象(所有继承自
- 解析器:
- 字段解析器(所有继承自
::Types::BaseResolver的类) - 变更(所有继承自
::Types::BaseMutation的类)
- 字段解析器(所有继承自
- 字段(所有使用
fieldDSL 方法声明的字段)
不能为抽象类型(接口和联合)指定授权。抽象类型将其授权委托给其成员类型。 基本内置标量(如整数)没有授权。
我们的授权系统使用与应用程序其他部分相同的 DeclarativePolicy 系统。
- 对于单个值(如
Query.project),如果当前认证用户未通过授权检查,该字段将解析为null。 - 对于集合(如
Project.issues),集合会被过滤以排除用户授权检查失败的对象。这个过滤过程(也称为 redaction)在分页之后进行,因此由于被移除的对象,某些页面可能比请求的页面大小要小。
另请参阅 在变更中授权资源。
最佳实践是首先使用现有的查找器仅加载当前认证用户有权查看的内容,而不依赖授权来过滤记录。这最小化了数据库查询和已加载记录的不必要授权检查。它还避免了可能暴露机密资源存在的情况,例如页面过短。
有关此处讨论的所有授权方案的示例,请参阅 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 权限。
要针对父对象授权,解析器必须 opt in(因为这最初不是默认值),通过使用 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
endMyType.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
endclass IssueType
field :author, UserType, authorize: :second_permission
endUserType 上的对象授权和 IssueType.author 上的字段授权的组合意味着以下测试:
Ability.allowed?(current_user, :second_permission, issue) &&
Ability.allowed?(current_user, :first_permission, issue.author)跳过给定字段的类型授权
在某些场景中,给定字段由专用的 resolver 解析,并且解析器负责检查解析对象的授权。
在这种情况下,特别是当字段解析对象集合时,我们希望跳过 Type 级别的授权。根据 GraphQL 查询,这些重叠的授权检查可能会带来显著的开销。
对于这种情况,我们可以通过在给定字段上使用 skip_type_authorization 指定应跳过哪些权限,来指定在 Type 级别跳过哪些权限。此选项会级联到所有后代字段。
有关实际示例,请参阅 field :discussions, Types::Notes::DiscussionType。
在该示例中,我们有 DiscussionType 指定了 authorize :read_note。Discussion 由多个 NoteType 类型的 notes 组成,并且 NoteType 也指定了 authorize: :read_note。
其中一些 notes 可能是系统笔记,并且可能具有特定类型的元数据 SystemNoteMetadataType。
SystemNoteMetadataType 也指定了 authorize: :read_note。每个笔记都可以有表情符号,这些表情符号使用 read_emoji 授权,在此情况下等同于 read_note。
要在 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 个笔记。并且每个讨论的第一个笔记有一个表情符号。
在这种情况下,我们在 SomeResolver 中授权讨论,即 10 次授权调用。
然后当我们用 DiscussionType 表示每个讨论时,我们授权每个讨论对象,又是 10 次调用。这些特定调用可能没问题,因为在解析器授权期间,这些对象已经被缓存在请求存储中,因为我们授权的是相同的对象。
接下来,我们为这 10 个讨论中的每个笔记进行授权,导致 10*10 = 100 次授权调用。最后,对于每个讨论的第一个笔记,我们将授权一个表情符号,即另外 10 次调用。所以总共有 130 次授权调用:
- 在解析器中授权 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中授权了讨论 - 对于讨论,读取单个笔记或所有笔记的权限是相同的
- 读取笔记或读取表情符号的权限是等效的