blob: 3d3f7212053d07a107f4ab1b1ab338b4b838e6a2 (
plain)
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
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
|
# frozen_string_literal: true
module Gitlab
module Auth
AuthenticationError = Class.new(StandardError)
MissingTokenError = Class.new(AuthenticationError)
TokenNotFoundError = Class.new(AuthenticationError)
ExpiredError = Class.new(AuthenticationError)
RevokedError = Class.new(AuthenticationError)
ImpersonationDisabled = Class.new(AuthenticationError)
UnauthorizedError = Class.new(AuthenticationError)
class InsufficientScopeError < AuthenticationError
attr_reader :scopes
def initialize(scopes)
@scopes = scopes.map { |s| s.try(:name) || s }
end
end
module AuthFinders
include Gitlab::Utils::StrongMemoize
include ActionController::HttpAuthentication::Basic
include ActionController::HttpAuthentication::Token
PRIVATE_TOKEN_HEADER = 'HTTP_PRIVATE_TOKEN'
PRIVATE_TOKEN_PARAM = :private_token
JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'.freeze
JOB_TOKEN_PARAM = :job_token
DEPLOY_TOKEN_HEADER = 'HTTP_DEPLOY_TOKEN'.freeze
RUNNER_TOKEN_PARAM = :token
RUNNER_JOB_TOKEN_PARAM = :token
# Check the Rails session for valid authentication details
def find_user_from_warden
current_request.env['warden']&.authenticate if verified_request?
end
def find_user_from_static_object_token(request_format)
return unless valid_static_objects_format?(request_format)
token = current_request.params[:token].presence || current_request.headers['X-Gitlab-Static-Object-Token'].presence
return unless token
User.find_by_static_object_token(token) || raise(UnauthorizedError)
end
def find_user_from_feed_token(request_format)
return unless valid_rss_format?(request_format)
# NOTE: feed_token was renamed from rss_token but both needs to be supported because
# users might have already added the feed to their RSS reader before the rename
token = current_request.params[:feed_token].presence || current_request.params[:rss_token].presence
return unless token
User.find_by_feed_token(token) || raise(UnauthorizedError)
end
def find_user_from_bearer_token
find_user_from_job_bearer_token ||
find_user_from_access_token
end
def find_user_from_job_token
return unless route_authentication_setting[:job_token_allowed]
return find_user_from_basic_auth_job if route_authentication_setting[:job_token_allowed] == :basic_auth
token = current_request.params[JOB_TOKEN_PARAM].presence ||
current_request.params[RUNNER_JOB_TOKEN_PARAM].presence ||
current_request.env[JOB_TOKEN_HEADER].presence
return unless token
job = find_valid_running_job_by_token!(token)
@current_authenticated_job = job # rubocop:disable Gitlab/ModuleWithInstanceVariables
job.user
end
def find_user_from_basic_auth_job
return unless has_basic_credentials?(current_request)
login, password = user_name_and_password(current_request)
return unless login.present? && password.present?
return unless ::Gitlab::Auth::CI_JOB_USER == login
job = find_valid_running_job_by_token!(password)
job.user
end
# We only allow Private Access Tokens with `api` scope to be used by web
# requests on RSS feeds or ICS files for backwards compatibility.
# It is also used by GraphQL/API requests.
def find_user_from_web_access_token(request_format)
return unless access_token && valid_web_access_format?(request_format)
validate_access_token!(scopes: [:api])
::PersonalAccessTokens::LastUsedService.new(access_token).execute
access_token.user || raise(UnauthorizedError)
end
def find_user_from_access_token
return unless access_token
validate_access_token!
::PersonalAccessTokens::LastUsedService.new(access_token).execute
access_token.user || raise(UnauthorizedError)
end
# This returns a deploy token, not a user since a deploy token does not
# belong to a user.
#
# deploy tokens are accepted with deploy token headers and basic auth headers
def deploy_token_from_request
return unless route_authentication_setting[:deploy_token_allowed]
token = current_request.env[DEPLOY_TOKEN_HEADER].presence || parsed_oauth_token
if has_basic_credentials?(current_request)
_, token = user_name_and_password(current_request)
end
deploy_token = DeployToken.active.find_by_token(token)
@current_authenticated_deploy_token = deploy_token # rubocop:disable Gitlab/ModuleWithInstanceVariables
deploy_token
end
def cluster_agent_token_from_authorization_token
return unless route_authentication_setting[:cluster_agent_token_allowed]
return unless current_request.authorization.present?
authorization_token, _options = token_and_options(current_request)
::Clusters::AgentToken.find_by_token(authorization_token)
end
def find_runner_from_token
return unless api_request?
token = current_request.params[RUNNER_TOKEN_PARAM].presence
return unless token
::Ci::Runner.find_by_token(token) || raise(UnauthorizedError)
end
def validate_access_token!(scopes: [])
# return early if we've already authenticated via a job token
return if @current_authenticated_job.present? # rubocop:disable Gitlab/ModuleWithInstanceVariables
# return early if we've already authenticated via a deploy token
return if @current_authenticated_deploy_token.present? # rubocop:disable Gitlab/ModuleWithInstanceVariables
return unless access_token
case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
when AccessTokenValidationService::INSUFFICIENT_SCOPE
raise InsufficientScopeError.new(scopes)
when AccessTokenValidationService::EXPIRED
raise ExpiredError
when AccessTokenValidationService::REVOKED
raise RevokedError
when AccessTokenValidationService::IMPERSONATION_DISABLED
raise ImpersonationDisabled
end
end
private
def find_user_from_job_bearer_token
return unless route_authentication_setting[:job_token_allowed]
token = parsed_oauth_token
return unless token
job = ::Ci::AuthJobFinder.new(token: token).execute
return unless job
@current_authenticated_job = job # rubocop:disable Gitlab/ModuleWithInstanceVariables
job.user
end
def route_authentication_setting
return {} unless respond_to?(:route_setting)
route_setting(:authentication) || {}
end
def access_token
strong_memoize(:access_token) do
find_oauth_access_token || find_personal_access_token
end
end
def find_personal_access_token
token =
current_request.params[PRIVATE_TOKEN_PARAM].presence ||
current_request.env[PRIVATE_TOKEN_HEADER].presence ||
parsed_oauth_token
return unless token
# Expiration, revocation and scopes are verified in `validate_access_token!`
PersonalAccessToken.find_by_token(token) || raise(UnauthorizedError)
end
def find_oauth_access_token
token = parsed_oauth_token
return unless token
# PATs with OAuth headers are not handled by OauthAccessToken
return if matches_personal_access_token_length?(token)
# Expiration, revocation and scopes are verified in `validate_access_token!`
oauth_token = OauthAccessToken.by_token(token)
raise UnauthorizedError unless oauth_token
oauth_token.revoke_previous_refresh_token!
oauth_token
end
def find_personal_access_token_from_http_basic_auth
return unless route_authentication_setting[:basic_auth_personal_access_token]
return unless has_basic_credentials?(current_request)
_username, password = user_name_and_password(current_request)
PersonalAccessToken.find_by_token(password)
end
def parsed_oauth_token
Doorkeeper::OAuth::Token.from_request(current_request, *Doorkeeper.configuration.access_token_methods)
end
def matches_personal_access_token_length?(token)
token.length == PersonalAccessToken::TOKEN_LENGTH
end
# Check if the request is GET/HEAD, or if CSRF token is valid.
def verified_request?
Gitlab::RequestForgeryProtection.verified?(current_request.env)
end
def ensure_action_dispatch_request(request)
ActionDispatch::Request.new(request.env.dup)
end
def current_request
@current_request ||= ensure_action_dispatch_request(request)
end
def valid_web_access_format?(request_format)
case request_format
when :rss
rss_request?
when :ics
ics_request?
when :api
api_request?
end
end
def valid_rss_format?(request_format)
case request_format
when :rss
rss_request?
when :ics
ics_request?
end
end
def valid_static_objects_format?(request_format)
case request_format
when :archive
archive_request?
when :blob
blob_request?
else
false
end
end
def rss_request?
current_request.path.ends_with?('.atom') || current_request.format.atom?
end
def ics_request?
current_request.path.ends_with?('.ics') || current_request.format.ics?
end
def api_request?
current_request.path.starts_with?(Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, '/api/'))
end
def archive_request?
current_request.path.include?('/-/archive/')
end
def blob_request?
current_request.path.include?('/raw/')
end
def find_valid_running_job_by_token!(token)
::Ci::AuthJobFinder.new(token: token).execute.tap do |job|
raise UnauthorizedError unless job
end
end
end
end
end
Gitlab::Auth::AuthFinders.prepend_if_ee('::EE::Gitlab::Auth::AuthFinders')
|