1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
# frozen_string_literal: true
module Gitlab
module Graphql
module Authorize
class AuthorizeFieldService
def initialize(field)
@field = field
@old_resolve_proc = @field.resolve_proc
end
def authorizations?
authorizations.present?
end
def authorized_resolve
proc do |parent_typed_object, args, ctx|
resolved_type = @old_resolve_proc.call(parent_typed_object, args, ctx)
authorizing_object = authorize_against(parent_typed_object, resolved_type)
filter_allowed(ctx[:current_user], resolved_type, authorizing_object)
end
end
private
def authorizations
@authorizations ||= (type_authorizations + field_authorizations).uniq
end
# Returns any authorize metadata from the return type of @field
def type_authorizations
type = @field.type
# When the return type of @field is a collection, find the singular type
if @field.connection?
type = node_type_for_relay_connection(type)
elsif type.list?
type = node_type_for_basic_connection(type)
end
type = type.unwrap if type.kind.non_null?
Array.wrap(type.metadata[:authorize])
end
# Returns any authorize metadata from @field
def field_authorizations
return [] if @field.metadata[:authorize] == true
Array.wrap(@field.metadata[:authorize])
end
def authorize_against(parent_typed_object, resolved_type)
if scalar_type?
# The field is a built-in/scalar type, or a list of scalars
# authorize using the parent's object
parent_typed_object.object
elsif @field.connection? || @field.type.list? || resolved_type.is_a?(Array)
# The field is a connection or a list of non-built-in types, we'll
# authorize each element when rendering
nil
elsif resolved_type.respond_to?(:object)
# The field is a type representing a single object, we'll authorize
# against the object directly
resolved_type.object
else
# Resolved type is a single object that might not be loaded yet by
# the batchloader, we'll authorize that
resolved_type
end
end
def filter_allowed(current_user, resolved_type, authorizing_object)
if resolved_type.nil?
# We're not rendering anything, for example when a record was not found
# no need to do anything
elsif authorizing_object
# Authorizing fields representing scalars, or a simple field with an object
::Gitlab::Graphql::Lazy.with_value(authorizing_object) do |object|
resolved_type if allowed_access?(current_user, object)
end
elsif @field.connection?
::Gitlab::Graphql::Lazy.with_value(resolved_type) do |type|
# A connection with pagination, modify the visible nodes on the
# connection type in place
nodes = to_nodes(type)
nodes.keep_if { |node| allowed_access?(current_user, node) } if nodes
type
end
elsif @field.type.list? || resolved_type.is_a?(Array)
# A simple list of rendered types each object being an object to authorize
::Gitlab::Graphql::Lazy.with_value(resolved_type) do |items|
items.select do |single_object_type|
object_type = realized(single_object_type)
object = object_type.try(:object) || object_type
allowed_access?(current_user, object)
end
end
else
raise "Can't authorize #{@field}"
end
end
# Ensure that we are dealing with realized objects, not delayed promises
def realized(thing)
::Gitlab::Graphql::Lazy.force(thing)
end
# Try to get the connection
# can be at type.object or at type
def to_nodes(type)
if type.respond_to?(:nodes)
type.nodes
elsif type.respond_to?(:object)
to_nodes(type.object)
else
nil
end
end
def allowed_access?(current_user, object)
object = realized(object)
authorizations.all? do |ability|
Ability.allowed?(current_user, ability, object)
end
end
# Returns the singular type for relay connections.
# This will be the type class of edges.node
def node_type_for_relay_connection(type)
type.unwrap.get_field('edges').type.unwrap.get_field('node').type
end
# Returns the singular type for basic connections, for example `[Types::ProjectType]`
def node_type_for_basic_connection(type)
type.unwrap
end
def scalar_type?
node_type_for_basic_connection(@field.type).kind.scalar?
end
end
end
end
end
|