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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
|
# frozen_string_literal: true
class GraphqlController < ApplicationController
extend ::Gitlab::Utils::Override
# Unauthenticated users have access to the API for public data
skip_before_action :authenticate_user!
# Header can be passed by tests to disable SQL query limits.
DISABLE_SQL_QUERY_LIMIT_HEADER = 'HTTP_X_GITLAB_DISABLE_SQL_QUERY_LIMIT'
# Max size of the query text in characters
MAX_QUERY_SIZE = 10_000
# If a user is using their session to access GraphQL, we need to have session
# storage, since the admin-mode check is session wide.
# We can't enable this for anonymous users because that would cause users using
# enforced SSO from using an auth token to access the API.
skip_around_action :set_session_storage, unless: :current_user
# Allow missing CSRF tokens, this would mean that if a CSRF is invalid or missing,
# the user won't be authenticated but can proceed as an anonymous user.
#
# If a CSRF is valid, the user is authenticated. This makes it easier to play
# around in GraphiQL.
protect_from_forgery with: :null_session, only: :execute
# must come first: current_user is set up here
before_action(only: [:execute]) { authenticate_sessionless_user!(:api) }
before_action :authorize_access_api!
before_action :set_user_last_activity
before_action :track_vs_code_usage
before_action :track_jetbrains_usage
before_action :track_gitlab_cli_usage
before_action :disable_query_limiting
before_action :limit_query_size
before_action :disallow_mutations_for_get
# Since we deactivate authentication from the main ApplicationController and
# defer it to :authorize_access_api!, we need to override the bypass session
# callback execution order here
around_action :sessionless_bypass_admin_mode!, if: :sessionless_user?
# The default feature category is overridden to read from request
feature_category :not_owned # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
# We don't know what the query is going to be, so we can't set a high urgency
# See https://gitlab.com/groups/gitlab-org/-/epics/5841 for the work that will
# allow us to specify an urgency per query.
# Currently, all queries have a default urgency. And this is measured in the `graphql_queries`
# SLI. But queries could be multiplexed, so the total duration could be longer.
urgency :low, [:execute]
def execute
result = multiplex? ? execute_multiplex : execute_query
render json: result
end
rescue_from StandardError do |exception|
@exception_object = exception
log_exception(exception)
if Rails.env.test? || Rails.env.development?
render_error("Internal server error: #{exception.message}", raised_at: exception.backtrace[0..10].join(' <-- '))
else
render_error("Internal server error")
end
end
rescue_from Gitlab::Graphql::Variables::Invalid do |exception|
render_error(exception.message, status: :unprocessable_entity)
end
rescue_from Gitlab::Graphql::Errors::ArgumentError do |exception|
render_error(exception.message, status: :unprocessable_entity)
end
rescue_from ::GraphQL::CoercionError do |exception|
render_error(exception.message, status: :unprocessable_entity)
end
rescue_from ActiveRecord::QueryAborted do |exception|
log_exception(exception)
error = "Request timed out. Please try a less complex query or a smaller set of records."
render_error(error, status: :service_unavailable)
end
override :feature_category
def feature_category
::Gitlab::FeatureCategories.default.from_request(request) || super
end
private
def disallow_mutations_for_get
return unless request.get? || request.head?
return unless any_mutating_query?
raise ::Gitlab::Graphql::Errors::ArgumentError, "Mutations are forbidden in #{request.request_method} requests"
end
def limit_query_size
total_size = if multiplex?
params[:_json].sum { _1[:query].size }
else
query.size
end
raise ::Gitlab::Graphql::Errors::ArgumentError, "Query too large" if total_size > MAX_QUERY_SIZE
end
def any_mutating_query?
if multiplex?
multiplex_queries.any? { |q| mutation?(q[:query], q[:operation_name]) }
else
mutation?(query)
end
end
def mutation?(query_string, operation_name = params[:operationName])
::GraphQL::Query.new(GitlabSchema, query_string, operation_name: operation_name).mutation?
end
# Tests may mark some GraphQL queries as exempt from SQL query limits
def disable_query_limiting
return unless Gitlab::QueryLimiting.enabled_for_env?
disable_issue = request.headers[DISABLE_SQL_QUERY_LIMIT_HEADER]
return unless disable_issue
Gitlab::QueryLimiting.disable!(disable_issue)
end
def set_user_last_activity
return unless current_user
Users::ActivityService.new(current_user).execute
end
def track_vs_code_usage
Gitlab::UsageDataCounters::VSCodeExtensionActivityUniqueCounter
.track_api_request_when_trackable(user_agent: request.user_agent, user: current_user)
end
def track_jetbrains_usage
Gitlab::UsageDataCounters::JetBrainsPluginActivityUniqueCounter
.track_api_request_when_trackable(user_agent: request.user_agent, user: current_user)
end
def track_gitlab_cli_usage
Gitlab::UsageDataCounters::GitLabCliActivityUniqueCounter
.track_api_request_when_trackable(user_agent: request.user_agent, user: current_user)
end
def execute_multiplex
GitlabSchema.multiplex(multiplex_queries, context: context)
end
def execute_query
variables = build_variables(params[:variables])
operation_name = params[:operationName]
GitlabSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
end
def query
params.fetch(:query, '')
end
def multiplex_queries
params[:_json].map do |single_query_info|
{
query: single_query_info[:query],
variables: build_variables(single_query_info[:variables]),
operation_name: single_query_info[:operationName],
context: context
}
end
end
# When modifying the context, also update GraphqlChannel#context if needed
# so that we have similar context when executing queries, mutations, and subscriptions
def context
api_user = !!sessionless_user?
@context ||= {
current_user: current_user,
is_sessionless_user: api_user,
request: request,
scope_validator: ::Gitlab::Auth::ScopeValidator.new(api_user, request_authenticator)
}
end
def build_variables(variable_info)
Gitlab::Graphql::Variables.new(variable_info).to_h
end
def multiplex?
params[:_json].present?
end
def authorize_access_api!
return if can?(current_user, :access_api)
render_error('API not accessible for user', status: :forbidden)
end
# Overridden from the ApplicationController to make the response look like
# a GraphQL response. That is nicely picked up in Graphiql.
def render_404
render_error("Not found!", status: :not_found)
end
def render_error(message, status: 500, raised_at: nil)
error = { errors: [message: message] }
error[:errors].first['raisedAt'] = raised_at if raised_at
render json: error, status: status
end
def append_info_to_payload(payload)
super
# Merging to :metadata will ensure these are logged as top level keys
payload[:metadata] ||= {}
payload[:metadata][:graphql] = logs
payload[:exception_object] = @exception_object if @exception_object
end
def logs
RequestStore.store[:graphql_logs].to_a
end
end
|