Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
eb396f71
Commit
eb396f71
authored
Nov 17, 2017
by
Francisco Javier López
Committed by
Douwe Maan
Nov 17, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
CE Port: Add request rate limits
parent
b4045045
Changes
19
Hide whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
1065 additions
and
168 deletions
+1065
-168
app/controllers/application_controller.rb
app/controllers/application_controller.rb
+5
-22
app/controllers/omniauth_callbacks_controller.rb
app/controllers/omniauth_callbacks_controller.rb
+4
-2
app/helpers/application_settings_helper.rb
app/helpers/application_settings_helper.rb
+9
-0
app/models/application_setting.rb
app/models/application_setting.rb
+9
-0
app/views/admin/application_settings/_form.html.haml
app/views/admin/application_settings/_form.html.haml
+51
-0
changelogs/unreleased/mk-add-user-rate-limits.yml
changelogs/unreleased/mk-add-user-rate-limits.yml
+6
-0
config/application.rb
config/application.rb
+1
-1
config/initializers/rack_attack_global.rb
config/initializers/rack_attack_global.rb
+61
-0
db/migrate/20171006220837_add_global_rate_limits_to_application_settings.rb
...6220837_add_global_rate_limits_to_application_settings.rb
+38
-0
db/schema.rb
db/schema.rb
+9
-0
lib/api/api_guard.rb
lib/api/api_guard.rb
+12
-117
lib/api/helpers.rb
lib/api/helpers.rb
+1
-1
lib/gitlab/auth/request_authenticator.rb
lib/gitlab/auth/request_authenticator.rb
+25
-0
lib/gitlab/auth/user_auth_finders.rb
lib/gitlab/auth/user_auth_finders.rb
+129
-0
spec/ee/spec/lib/ee/api/helpers_spec.rb
spec/ee/spec/lib/ee/api/helpers_spec.rb
+1
-0
spec/lib/gitlab/auth/request_authenticator_spec.rb
spec/lib/gitlab/auth/request_authenticator_spec.rb
+67
-0
spec/lib/gitlab/auth/user_auth_finders_spec.rb
spec/lib/gitlab/auth/user_auth_finders_spec.rb
+244
-0
spec/requests/api/helpers_spec.rb
spec/requests/api/helpers_spec.rb
+31
-25
spec/requests/rack_attack_global_spec.rb
spec/requests/rack_attack_global_spec.rb
+362
-0
No files found.
app/controllers/application_controller.rb
View file @
eb396f71
...
...
@@ -11,8 +11,7 @@ class ApplicationController < ActionController::Base
include
EnforcesTwoFactorAuthentication
include
WithPerformanceBar
before_action
:authenticate_user_from_personal_access_token!
before_action
:authenticate_user_from_rss_token!
before_action
:authenticate_sessionless_user!
before_action
:authenticate_user!
before_action
:validate_user_service_ticket!
before_action
:check_password_expiration
...
...
@@ -100,27 +99,11 @@ class ApplicationController < ActionController::Base
return
try
(
:authenticated_user
)
end
def
authenticate_user_from_personal_access_token!
token
=
params
[
:private_token
].
presence
||
request
.
headers
[
'PRIVATE-TOKEN'
].
presence
# This filter handles personal access tokens, and atom requests with rss tokens
def
authenticate_sessionless_user!
user
=
Gitlab
::
Auth
::
RequestAuthenticator
.
new
(
request
).
find_sessionless_user
return
unless
token
.
present?
user
=
User
.
find_by_personal_access_token
(
token
)
sessionless_sign_in
(
user
)
end
# This filter handles authentication for atom request with an rss_token
def
authenticate_user_from_rss_token!
return
unless
request
.
format
.
atom?
token
=
params
[
:rss_token
].
presence
return
unless
token
.
present?
user
=
User
.
find_by_rss_token
(
token
)
sessionless_sign_in
(
user
)
sessionless_sign_in
(
user
)
if
user
end
def
verify_namespace_plan_check_enabled
...
...
app/controllers/omniauth_callbacks_controller.rb
View file @
eb396f71
...
...
@@ -57,7 +57,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
if
current_user
log_audit_event
(
current_user
,
with: :saml
)
# Update SAML identity if data has changed.
identity
=
current_user
.
identities
.
find_by
(
extern_uid:
oauth
[
'uid'
],
provider: :saml
)
identity
=
current_user
.
identities
.
with_extern_uid
(
:saml
,
oauth
[
'uid'
]).
take
if
identity
.
nil?
current_user
.
identities
.
create
(
extern_uid:
oauth
[
'uid'
],
provider: :saml
)
redirect_to
profile_account_path
,
notice:
'Authentication method updated'
...
...
@@ -112,7 +112,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def
handle_omniauth
if
current_user
# Add new authentication method
current_user
.
identities
.
find_or_create_by
(
extern_uid:
oauth
[
'uid'
],
provider:
oauth
[
'provider'
])
current_user
.
identities
.
with_extern_uid
(
oauth
[
'provider'
],
oauth
[
'uid'
])
.
first_or_create
(
extern_uid:
oauth
[
'uid'
])
log_audit_event
(
current_user
,
with:
oauth
[
'provider'
])
redirect_to
profile_account_path
,
notice:
'Authentication method updated'
else
...
...
app/helpers/application_settings_helper.rb
View file @
eb396f71
...
...
@@ -232,6 +232,15 @@ module ApplicationSettingsHelper
:sign_in_text
,
:signup_enabled
,
:terminal_max_session_time
,
:throttle_unauthenticated_enabled
,
:throttle_unauthenticated_requests_per_period
,
:throttle_unauthenticated_period_in_seconds
,
:throttle_authenticated_web_enabled
,
:throttle_authenticated_web_requests_per_period
,
:throttle_authenticated_web_period_in_seconds
,
:throttle_authenticated_api_enabled
,
:throttle_authenticated_api_requests_per_period
,
:throttle_authenticated_api_period_in_seconds
,
:two_factor_grace_period
,
:unique_ips_limit_enabled
,
:unique_ips_limit_per_user
,
...
...
app/models/application_setting.rb
View file @
eb396f71
...
...
@@ -308,6 +308,15 @@ class ApplicationSetting < ActiveRecord::Base
sign_in_text:
nil
,
signup_enabled:
Settings
.
gitlab
[
'signup_enabled'
],
terminal_max_session_time:
0
,
throttle_unauthenticated_enabled:
false
,
throttle_unauthenticated_requests_per_period:
3600
,
throttle_unauthenticated_period_in_seconds:
3600
,
throttle_authenticated_web_enabled:
false
,
throttle_authenticated_web_requests_per_period:
7200
,
throttle_authenticated_web_period_in_seconds:
3600
,
throttle_authenticated_api_enabled:
false
,
throttle_authenticated_api_requests_per_period:
7200
,
throttle_authenticated_api_period_in_seconds:
3600
,
two_factor_grace_period:
48
,
user_default_external:
false
,
polling_interval_multiplier:
1
,
...
...
app/views/admin/application_settings/_form.html.haml
View file @
eb396f71
...
...
@@ -835,5 +835,56 @@
The amount of seconds after which a request to get a secondary node
status will time out.
%fieldset
%legend
User and IP Rate Limits
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
=
f
.
label
:throttle_unauthenticated_enabled
do
=
f
.
check_box
:throttle_unauthenticated_enabled
Enable unauthenticated request rate limit
%span
.help-block
Helps reduce request volume (e.g. from crawlers or abusive bots)
.form-group
=
f
.
label
:throttle_unauthenticated_requests_per_period
,
'Max requests per period per IP'
,
class:
'control-label col-sm-2'
.col-sm-10
=
f
.
number_field
:throttle_unauthenticated_requests_per_period
,
class:
'form-control'
.form-group
=
f
.
label
:throttle_unauthenticated_period_in_seconds
,
'Rate limit period in seconds'
,
class:
'control-label col-sm-2'
.col-sm-10
=
f
.
number_field
:throttle_unauthenticated_period_in_seconds
,
class:
'form-control'
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
=
f
.
label
:throttle_authenticated_api_enabled
do
=
f
.
check_box
:throttle_authenticated_api_enabled
Enable authenticated API request rate limit
%span
.help-block
Helps reduce request volume (e.g. from crawlers or abusive bots)
.form-group
=
f
.
label
:throttle_authenticated_api_requests_per_period
,
'Max requests per period per user'
,
class:
'control-label col-sm-2'
.col-sm-10
=
f
.
number_field
:throttle_authenticated_api_requests_per_period
,
class:
'form-control'
.form-group
=
f
.
label
:throttle_authenticated_api_period_in_seconds
,
'Rate limit period in seconds'
,
class:
'control-label col-sm-2'
.col-sm-10
=
f
.
number_field
:throttle_authenticated_api_period_in_seconds
,
class:
'form-control'
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
=
f
.
label
:throttle_authenticated_web_enabled
do
=
f
.
check_box
:throttle_authenticated_web_enabled
Enable authenticated web request rate limit
%span
.help-block
Helps reduce request volume (e.g. from crawlers or abusive bots)
.form-group
=
f
.
label
:throttle_authenticated_web_requests_per_period
,
'Max requests per period per user'
,
class:
'control-label col-sm-2'
.col-sm-10
=
f
.
number_field
:throttle_authenticated_web_requests_per_period
,
class:
'form-control'
.form-group
=
f
.
label
:throttle_authenticated_web_period_in_seconds
,
'Rate limit period in seconds'
,
class:
'control-label col-sm-2'
.col-sm-10
=
f
.
number_field
:throttle_authenticated_web_period_in_seconds
,
class:
'form-control'
.form-actions
=
f
.
submit
'Save'
,
class:
'btn btn-save'
changelogs/unreleased/mk-add-user-rate-limits.yml
0 → 100644
View file @
eb396f71
---
title
:
Add anonymous rate limit per IP, and authenticated (web or API) rate limits
per user
merge_request
:
14708
author
:
type
:
added
config/application.rb
View file @
eb396f71
...
...
@@ -130,7 +130,7 @@ module Gitlab
config
.
action_view
.
sanitized_allowed_protocols
=
%w(smb)
config
.
middleware
.
insert_
before
Warden
::
Manager
,
Rack
::
Attack
config
.
middleware
.
insert_
after
Warden
::
Manager
,
Rack
::
Attack
# Allow access to GitLab API from other domains
config
.
middleware
.
insert_before
Warden
::
Manager
,
Rack
::
Cors
do
...
...
config/initializers/rack_attack_global.rb
0 → 100644
View file @
eb396f71
module
Gitlab::Throttle
def
self
.
settings
Gitlab
::
CurrentSettings
.
current_application_settings
end
def
self
.
unauthenticated_options
limit_proc
=
proc
{
|
req
|
settings
.
throttle_unauthenticated_requests_per_period
}
period_proc
=
proc
{
|
req
|
settings
.
throttle_unauthenticated_period_in_seconds
.
seconds
}
{
limit:
limit_proc
,
period:
period_proc
}
end
def
self
.
authenticated_api_options
limit_proc
=
proc
{
|
req
|
settings
.
throttle_authenticated_api_requests_per_period
}
period_proc
=
proc
{
|
req
|
settings
.
throttle_authenticated_api_period_in_seconds
.
seconds
}
{
limit:
limit_proc
,
period:
period_proc
}
end
def
self
.
authenticated_web_options
limit_proc
=
proc
{
|
req
|
settings
.
throttle_authenticated_web_requests_per_period
}
period_proc
=
proc
{
|
req
|
settings
.
throttle_authenticated_web_period_in_seconds
.
seconds
}
{
limit:
limit_proc
,
period:
period_proc
}
end
end
class
Rack::Attack
throttle
(
'throttle_unauthenticated'
,
Gitlab
::
Throttle
.
unauthenticated_options
)
do
|
req
|
Gitlab
::
Throttle
.
settings
.
throttle_unauthenticated_enabled
&&
req
.
unauthenticated?
&&
req
.
ip
end
throttle
(
'throttle_authenticated_api'
,
Gitlab
::
Throttle
.
authenticated_api_options
)
do
|
req
|
Gitlab
::
Throttle
.
settings
.
throttle_authenticated_api_enabled
&&
req
.
api_request?
&&
req
.
authenticated_user_id
end
throttle
(
'throttle_authenticated_web'
,
Gitlab
::
Throttle
.
authenticated_web_options
)
do
|
req
|
Gitlab
::
Throttle
.
settings
.
throttle_authenticated_web_enabled
&&
req
.
web_request?
&&
req
.
authenticated_user_id
end
class
Request
def
unauthenticated?
!
authenticated_user_id
end
def
authenticated_user_id
Gitlab
::
Auth
::
RequestAuthenticator
.
new
(
self
).
user
&
.
id
end
def
api_request?
path
.
start_with?
(
'/api'
)
end
def
web_request?
!
api_request?
end
end
end
db/migrate/20171006220837_add_global_rate_limits_to_application_settings.rb
0 → 100644
View file @
eb396f71
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class
AddGlobalRateLimitsToApplicationSettings
<
ActiveRecord
::
Migration
include
Gitlab
::
Database
::
MigrationHelpers
DOWNTIME
=
false
disable_ddl_transaction!
def
up
add_column_with_default
:application_settings
,
:throttle_unauthenticated_enabled
,
:boolean
,
default:
false
,
allow_null:
false
add_column_with_default
:application_settings
,
:throttle_unauthenticated_requests_per_period
,
:integer
,
default:
3600
,
allow_null:
false
add_column_with_default
:application_settings
,
:throttle_unauthenticated_period_in_seconds
,
:integer
,
default:
3600
,
allow_null:
false
add_column_with_default
:application_settings
,
:throttle_authenticated_api_enabled
,
:boolean
,
default:
false
,
allow_null:
false
add_column_with_default
:application_settings
,
:throttle_authenticated_api_requests_per_period
,
:integer
,
default:
7200
,
allow_null:
false
add_column_with_default
:application_settings
,
:throttle_authenticated_api_period_in_seconds
,
:integer
,
default:
3600
,
allow_null:
false
add_column_with_default
:application_settings
,
:throttle_authenticated_web_enabled
,
:boolean
,
default:
false
,
allow_null:
false
add_column_with_default
:application_settings
,
:throttle_authenticated_web_requests_per_period
,
:integer
,
default:
7200
,
allow_null:
false
add_column_with_default
:application_settings
,
:throttle_authenticated_web_period_in_seconds
,
:integer
,
default:
3600
,
allow_null:
false
end
def
down
remove_column
:application_settings
,
:throttle_authenticated_web_period_in_seconds
remove_column
:application_settings
,
:throttle_authenticated_web_requests_per_period
remove_column
:application_settings
,
:throttle_authenticated_web_enabled
remove_column
:application_settings
,
:throttle_authenticated_api_period_in_seconds
remove_column
:application_settings
,
:throttle_authenticated_api_requests_per_period
remove_column
:application_settings
,
:throttle_authenticated_api_enabled
remove_column
:application_settings
,
:throttle_unauthenticated_period_in_seconds
remove_column
:application_settings
,
:throttle_unauthenticated_requests_per_period
remove_column
:application_settings
,
:throttle_unauthenticated_enabled
end
end
db/schema.rb
View file @
eb396f71
...
...
@@ -164,6 +164,15 @@ ActiveRecord::Schema.define(version: 20171107144726) do
t
.
boolean
"remote_mirror_available"
,
default:
true
,
null:
false
t
.
integer
"circuitbreaker_access_retries"
,
default:
3
t
.
integer
"circuitbreaker_backoff_threshold"
,
default:
80
t
.
boolean
"throttle_unauthenticated_enabled"
,
default:
false
,
null:
false
t
.
integer
"throttle_unauthenticated_requests_per_period"
,
default:
3600
,
null:
false
t
.
integer
"throttle_unauthenticated_period_in_seconds"
,
default:
3600
,
null:
false
t
.
boolean
"throttle_authenticated_api_enabled"
,
default:
false
,
null:
false
t
.
integer
"throttle_authenticated_api_requests_per_period"
,
default:
7200
,
null:
false
t
.
integer
"throttle_authenticated_api_period_in_seconds"
,
default:
3600
,
null:
false
t
.
boolean
"throttle_authenticated_web_enabled"
,
default:
false
,
null:
false
t
.
integer
"throttle_authenticated_web_requests_per_period"
,
default:
7200
,
null:
false
t
.
integer
"throttle_authenticated_web_period_in_seconds"
,
default:
3600
,
null:
false
end
create_table
"approvals"
,
force: :cascade
do
|
t
|
...
...
lib/api/api_guard.rb
View file @
eb396f71
...
...
@@ -6,11 +6,6 @@ module API
module
APIGuard
extend
ActiveSupport
::
Concern
PRIVATE_TOKEN_HEADER
=
"HTTP_PRIVATE_TOKEN"
.
freeze
PRIVATE_TOKEN_PARAM
=
:private_token
JOB_TOKEN_HEADER
=
"HTTP_JOB_TOKEN"
.
freeze
JOB_TOKEN_PARAM
=
:job_token
included
do
|
base
|
# OAuth2 Resource Server Authentication
use
Rack
::
OAuth2
::
Server
::
Resource
::
Bearer
,
'The API'
do
|
request
|
...
...
@@ -44,6 +39,8 @@ module API
# Helper Methods for Grape Endpoint
module
HelperMethods
include
Gitlab
::
Auth
::
UserAuthFinders
def
find_current_user!
user
=
find_user_from_access_token
||
find_user_from_job_token
||
find_user_from_warden
return
unless
user
...
...
@@ -53,96 +50,8 @@ module API
user
end
def
access_token
return
@access_token
if
defined?
(
@access_token
)
@access_token
=
find_oauth_access_token
||
find_personal_access_token
end
def
validate_access_token!
(
scopes:
[])
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
end
end
private
def
find_user_from_access_token
return
unless
access_token
validate_access_token!
access_token
.
user
||
raise
(
UnauthorizedError
)
end
def
find_user_from_job_token
return
unless
route_authentication_setting
[
:job_token_allowed
]
token
=
(
params
[
JOB_TOKEN_PARAM
]
||
env
[
JOB_TOKEN_HEADER
]).
to_s
return
unless
token
.
present?
job
=
Ci
::
Build
.
find_by
(
token:
token
)
raise
UnauthorizedError
unless
job
@job_token_authentication
=
true
job
.
user
end
# Check the Rails session for valid authentication details
def
find_user_from_warden
warden
.
try
(
:authenticate
)
if
verified_request?
end
def
warden
env
[
'warden'
]
end
# Check if the request is GET/HEAD, or if CSRF token is valid.
def
verified_request?
Gitlab
::
RequestForgeryProtection
.
verified?
(
env
)
end
def
route_authentication_setting
return
{}
unless
respond_to?
(
:route_setting
)
route_setting
(
:authentication
)
||
{}
end
def
find_oauth_access_token
token
=
Doorkeeper
::
OAuth
::
Token
.
from_request
(
doorkeeper_request
,
*
Doorkeeper
.
configuration
.
access_token_methods
)
return
unless
token
# Expiration, revocation and scopes are verified in `find_user_by_access_token`
access_token
=
OauthAccessToken
.
by_token
(
token
)
raise
UnauthorizedError
unless
access_token
access_token
.
revoke_previous_refresh_token!
access_token
end
def
find_personal_access_token
token
=
(
params
[
PRIVATE_TOKEN_PARAM
]
||
env
[
PRIVATE_TOKEN_HEADER
]).
to_s
return
unless
token
.
present?
# Expiration, revocation and scopes are verified in `find_user_by_access_token`
access_token
=
PersonalAccessToken
.
find_by
(
token:
token
)
raise
UnauthorizedError
unless
access_token
access_token
end
def
doorkeeper_request
@doorkeeper_request
||=
ActionDispatch
::
Request
.
new
(
env
)
end
# An array of scopes that were registered (using `allow_access_with_scope`)
# for the current endpoint class. It also returns scopes registered on
# `API::API`, since these are meant to apply to all API routes.
...
...
@@ -165,8 +74,11 @@ module API
private
def
install_error_responders
(
base
)
error_classes
=
[
MissingTokenError
,
TokenNotFoundError
,
ExpiredError
,
RevokedError
,
InsufficientScopeError
]
error_classes
=
[
Gitlab
::
Auth
::
MissingTokenError
,
Gitlab
::
Auth
::
TokenNotFoundError
,
Gitlab
::
Auth
::
ExpiredError
,
Gitlab
::
Auth
::
RevokedError
,
Gitlab
::
Auth
::
InsufficientScopeError
]
base
.
__send__
(
:rescue_from
,
*
error_classes
,
oauth2_bearer_token_error_handler
)
# rubocop:disable GitlabSecurity/PublicSend
end
...
...
@@ -175,25 +87,25 @@ module API
proc
do
|
e
|
response
=
case
e
when
MissingTokenError
when
Gitlab
::
Auth
::
MissingTokenError
Rack
::
OAuth2
::
Server
::
Resource
::
Bearer
::
Unauthorized
.
new
when
TokenNotFoundError
when
Gitlab
::
Auth
::
TokenNotFoundError
Rack
::
OAuth2
::
Server
::
Resource
::
Bearer
::
Unauthorized
.
new
(
:invalid_token
,
"Bad Access Token."
)
when
ExpiredError
when
Gitlab
::
Auth
::
ExpiredError
Rack
::
OAuth2
::
Server
::
Resource
::
Bearer
::
Unauthorized
.
new
(
:invalid_token
,
"Token is expired. You can either do re-authorization or token refresh."
)
when
RevokedError
when
Gitlab
::
Auth
::
RevokedError
Rack
::
OAuth2
::
Server
::
Resource
::
Bearer
::
Unauthorized
.
new
(
:invalid_token
,
"Token was revoked. You have to re-authorize from the user."
)
when
InsufficientScopeError
when
Gitlab
::
Auth
::
InsufficientScopeError
# FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2)
# does not include WWW-Authenticate header, which breaks the standard.
Rack
::
OAuth2
::
Server
::
Resource
::
Bearer
::
Forbidden
.
new
(
...
...
@@ -206,22 +118,5 @@ module API
end
end
end
#
# Exceptions
#
MissingTokenError
=
Class
.
new
(
StandardError
)
TokenNotFoundError
=
Class
.
new
(
StandardError
)
ExpiredError
=
Class
.
new
(
StandardError
)
RevokedError
=
Class
.
new
(
StandardError
)
UnauthorizedError
=
Class
.
new
(
StandardError
)
class
InsufficientScopeError
<
StandardError
attr_reader
:scopes
def
initialize
(
scopes
)
@scopes
=
scopes
.
map
{
|
s
|
s
.
try
(
:name
)
||
s
}
end
end
end
end
lib/api/helpers.rb
View file @
eb396f71
...
...
@@ -431,7 +431,7 @@ module API
begin
@initial_current_user
=
Gitlab
::
Auth
::
UniqueIpsLimiter
.
limit_user!
{
find_current_user!
}
rescue
APIGuard
::
UnauthorizedError
rescue
Gitlab
::
Auth
::
UnauthorizedError
unauthorized!
end
end
...
...
lib/gitlab/auth/request_authenticator.rb
0 → 100644
View file @
eb396f71
# Use for authentication only, in particular for Rack::Attack.
# Does not perform authorization of scopes, etc.
module
Gitlab
module
Auth
class
RequestAuthenticator
include
UserAuthFinders
attr_reader
:request
def
initialize
(
request
)
@request
=
request
end
def
user
find_sessionless_user
||
find_user_from_warden
end
def
find_sessionless_user
find_user_from_access_token
||
find_user_from_rss_token
rescue
Gitlab
::
Auth
::
AuthenticationError
nil
end
end
end
end
lib/gitlab/auth/user_auth_finders.rb
0 → 100644
View file @
eb396f71
module
Gitlab
module
Auth
#
# Exceptions
#
AuthenticationError
=
Class
.
new
(
StandardError
)
MissingTokenError
=
Class
.
new
(
AuthenticationError
)
TokenNotFoundError
=
Class
.
new
(
AuthenticationError
)
ExpiredError
=
Class
.
new
(
AuthenticationError
)
RevokedError
=
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
UserAuthFinders
PRIVATE_TOKEN_HEADER
=
'HTTP_PRIVATE_TOKEN'
.
freeze
PRIVATE_TOKEN_PARAM
=
:private_token
JOB_TOKEN_HEADER
=
"HTTP_JOB_TOKEN"
.
freeze
JOB_TOKEN_PARAM
=
:job_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_rss_token
return
unless
current_request
.
path
.
ends_with?
(
'.atom'
)
||
current_request
.
format
.
atom?
token
=
current_request
.
params
[
:rss_token
].
presence
return
unless
token
User
.
find_by_rss_token
(
token
)
||
raise
(
UnauthorizedError
)
end
def
find_user_from_access_token
return
unless
access_token
validate_access_token!
access_token
.
user
||
raise
(
UnauthorizedError
)
end
def
find_user_from_job_token
return
unless
route_authentication_setting
[
:job_token_allowed
]
token
=
(
params
[
JOB_TOKEN_PARAM
]
||
env
[
JOB_TOKEN_HEADER
]).
to_s
return
unless
token
.
present?
job
=
::
Ci
::
Build
.
find_by
(
token:
token
)
raise
UnauthorizedError
unless
job
@job_token_authentication
=
true
job
.
user
end
def
validate_access_token!
(
scopes:
[])
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
end
end
private
def
route_authentication_setting
return
{}
unless
respond_to?
(
:route_setting
)
route_setting
(
:authentication
)
||
{}
end
def
access_token
return
@access_token
if
defined?
(
@access_token
)
@access_token
=
find_oauth_access_token
||
find_personal_access_token
end
def
find_personal_access_token
token
=
current_request
.
params
[
PRIVATE_TOKEN_PARAM
].
presence
||
current_request
.
env
[
PRIVATE_TOKEN_HEADER
].
presence
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
=
Doorkeeper
::
OAuth
::
Token
.
from_request
(
current_request
,
*
Doorkeeper
.
configuration
.
access_token_methods
)
return
unless
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
# 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
)
return
request
if
request
.
is_a?
(
ActionDispatch
::
Request
)
ActionDispatch
::
Request
.
new
(
request
.
env
)
end
def
current_request
@current_request
||=
ensure_action_dispatch_request
(
request
)
end
end
end
end
spec/ee/spec/lib/ee/api/helpers_spec.rb
View file @
eb396f71
...
...
@@ -13,6 +13,7 @@ describe EE::API::Helpers do
}
end
let
(
:header
)
{
}
let
(
:request
)
{
Grape
::
Request
.
new
(
env
)}
before
do
allow
(
Gitlab
::
Database
::
LoadBalancing
).
to
receive
(
:enable?
).
and_return
(
true
)
...
...
spec/lib/gitlab/auth/request_authenticator_spec.rb
0 → 100644
View file @
eb396f71
require
'spec_helper'
describe
Gitlab
::
Auth
::
RequestAuthenticator
do
let
(
:env
)
do
{
'rack.input'
=>
''
,
'REQUEST_METHOD'
=>
'GET'
}
end
let
(
:request
)
{
ActionDispatch
::
Request
.
new
(
env
)
}
subject
{
described_class
.
new
(
request
)
}
describe
'#user'
do
let!
(
:sessionless_user
)
{
build
(
:user
)
}
let!
(
:session_user
)
{
build
(
:user
)
}
it
'returns sessionless user first'
do
allow_any_instance_of
(
described_class
).
to
receive
(
:find_sessionless_user
).
and_return
(
sessionless_user
)
allow_any_instance_of
(
described_class
).
to
receive
(
:find_user_from_warden
).
and_return
(
session_user
)
expect
(
subject
.
user
).
to
eq
sessionless_user
end
it
'returns session user if no sessionless user found'
do
allow_any_instance_of
(
described_class
).
to
receive
(
:find_user_from_warden
).
and_return
(
session_user
)
expect
(
subject
.
user
).
to
eq
session_user
end
it
'returns nil if no user found'
do
expect
(
subject
.
user
).
to
be_blank
end
it
'bubbles up exceptions'
do
allow_any_instance_of
(
described_class
).
to
receive
(
:find_user_from_warden
).
and_raise
(
Gitlab
::
Auth
::
UnauthorizedError
)
end
end
describe
'#find_sessionless_user'
do
let!
(
:access_token_user
)
{
build
(
:user
)
}
let!
(
:rss_token_user
)
{
build
(
:user
)
}
it
'returns access_token user first'
do
allow_any_instance_of
(
described_class
).
to
receive
(
:find_user_from_access_token
).
and_return
(
access_token_user
)
allow_any_instance_of
(
described_class
).
to
receive
(
:find_user_from_rss_token
).
and_return
(
rss_token_user
)
expect
(
subject
.
find_sessionless_user
).
to
eq
access_token_user
end
it
'returns rss_token user if no access_token user found'
do
allow_any_instance_of
(
described_class
).
to
receive
(
:find_user_from_rss_token
).
and_return
(
rss_token_user
)
expect
(
subject
.
find_sessionless_user
).
to
eq
rss_token_user
end
it
'returns nil if no user found'
do
expect
(
subject
.
find_sessionless_user
).
to
be_blank
end
it
'rescue Gitlab::Auth::AuthenticationError exceptions'
do
allow_any_instance_of
(
described_class
).
to
receive
(
:find_user_from_access_token
).
and_raise
(
Gitlab
::
Auth
::
UnauthorizedError
)
expect
(
subject
.
find_sessionless_user
).
to
be_blank
end
end
end
spec/lib/gitlab/auth/user_auth_finders_spec.rb
0 → 100644
View file @
eb396f71
require
'spec_helper'
describe
Gitlab
::
Auth
::
UserAuthFinders
do
include
described_class
let
(
:user
)
{
create
(
:user
)
}
let
(
:env
)
do
{
'rack.input'
=>
''
}
end
let
(
:request
)
{
Rack
::
Request
.
new
(
env
)}
let
(
:params
)
{
request
.
params
}
def
set_param
(
key
,
value
)
request
.
update_param
(
key
,
value
)
end
describe
'#find_user_from_warden'
do
context
'with CSRF token'
do
before
do
allow
(
Gitlab
::
RequestForgeryProtection
).
to
receive
(
:verified?
).
and_return
(
true
)
end
context
'with invalid credentials'
do
it
'returns nil'
do
expect
(
find_user_from_warden
).
to
be_nil
end
end
context
'with valid credentials'
do
it
'returns the user'
do
env
[
'warden'
]
=
double
(
"warden"
,
authenticate:
user
)
expect
(
find_user_from_warden
).
to
eq
user
end
end
end
context
'without CSRF token'
do
it
'returns nil'
do
allow
(
Gitlab
::
RequestForgeryProtection
).
to
receive
(
:verified?
).
and_return
(
false
)
env
[
'warden'
]
=
double
(
"warden"
,
authenticate:
user
)
expect
(
find_user_from_warden
).
to
be_nil
end
end
end
describe
'#find_user_from_rss_token'
do
context
'when the request format is atom'
do
before
do
env
[
'HTTP_ACCEPT'
]
=
'application/atom+xml'
end
it
'returns user if valid rss_token'
do
set_param
(
:rss_token
,
user
.
rss_token
)
expect
(
find_user_from_rss_token
).
to
eq
user
end
it
'returns nil if rss_token is blank'
do
expect
(
find_user_from_rss_token
).
to
be_nil
end
it
'returns exception if invalid rss_token'
do
set_param
(
:rss_token
,
'invalid_token'
)
expect
{
find_user_from_rss_token
}.
to
raise_error
(
Gitlab
::
Auth
::
UnauthorizedError
)
end
end
context
'when the request format is not atom'
do
it
'returns nil'
do
set_param
(
:rss_token
,
user
.
rss_token
)
expect
(
find_user_from_rss_token
).
to
be_nil
end
end
end
describe
'#find_user_from_access_token'
do
let
(
:personal_access_token
)
{
create
(
:personal_access_token
,
user:
user
)
}
it
'returns nil if no access_token present'
do
expect
(
find_personal_access_token
).
to
be_nil
end
context
'when validate_access_token! returns valid'
do
it
'returns user'
do
env
[
Gitlab
::
Auth
::
UserAuthFinders
::
PRIVATE_TOKEN_HEADER
]
=
personal_access_token
.
token
expect
(
find_user_from_access_token
).
to
eq
user
end
it
'returns exception if token has no user'
do
env
[
Gitlab
::
Auth
::
UserAuthFinders
::
PRIVATE_TOKEN_HEADER
]
=
personal_access_token
.
token
allow_any_instance_of
(
PersonalAccessToken
).
to
receive
(
:user
).
and_return
(
nil
)
expect
{
find_user_from_access_token
}.
to
raise_error
(
Gitlab
::
Auth
::
UnauthorizedError
)
end
end
end
describe
'#find_user_from_job_token'
do
let
(
:job
)
{
create
(
:ci_build
,
user:
user
)
}
shared_examples
'find user from job token'
do
context
'when route is allowed to be authenticated'
do
let
(
:route_authentication_setting
)
{
{
job_token_allowed:
true
}
}
it
"returns an Unauthorized exception for an invalid token"
do
set_token
(
'invalid token'
)
expect
{
find_user_from_job_token
}.
to
raise_error
(
Gitlab
::
Auth
::
UnauthorizedError
)
end
it
"return user if token is valid"
do
set_token
(
job
.
token
)
expect
(
find_user_from_job_token
).
to
eq
(
user
)
end
end
context
'when route is not allowed to be authenticated'
do
let
(
:route_authentication_setting
)
{
{
job_token_allowed:
false
}
}
it
"sets current_user to nil"
do
set_token
(
job
.
token
)
allow_any_instance_of
(
Gitlab
::
UserAccess
).
to
receive
(
:allowed?
).
and_return
(
true
)
expect
(
find_user_from_job_token
).
to
be_nil
end
end
end
context
'when the job token is in the headers'
do
def
set_token
(
token
)
env
[
Gitlab
::
Auth
::
UserAuthFinders
::
JOB_TOKEN_HEADER
]
=
token
end
it_behaves_like
'find user from job token'
end
context
'when the job token is in the params'
do
def
set_token
(
token
)
set_param
(
Gitlab
::
Auth
::
UserAuthFinders
::
JOB_TOKEN_PARAM
,
token
)
end
it_behaves_like
'find user from job token'
end
end
describe
'#find_personal_access_token'
do
let
(
:personal_access_token
)
{
create
(
:personal_access_token
,
user:
user
)
}
context
'passed as header'
do
it
'returns token if valid personal_access_token'
do
env
[
Gitlab
::
Auth
::
UserAuthFinders
::
PRIVATE_TOKEN_HEADER
]
=
personal_access_token
.
token
expect
(
find_personal_access_token
).
to
eq
personal_access_token
end
end
context
'passed as param'
do
it
'returns token if valid personal_access_token'
do
set_param
(
Gitlab
::
Auth
::
UserAuthFinders
::
PRIVATE_TOKEN_PARAM
,
personal_access_token
.
token
)
expect
(
find_personal_access_token
).
to
eq
personal_access_token
end
end
it
'returns nil if no personal_access_token'
do
expect
(
find_personal_access_token
).
to
be_nil
end
it
'returns exception if invalid personal_access_token'
do
env
[
Gitlab
::
Auth
::
UserAuthFinders
::
PRIVATE_TOKEN_HEADER
]
=
'invalid_token'
expect
{
find_personal_access_token
}.
to
raise_error
(
Gitlab
::
Auth
::
UnauthorizedError
)
end
end
describe
'#find_oauth_access_token'
do
let
(
:application
)
{
Doorkeeper
::
Application
.
create!
(
name:
'MyApp'
,
redirect_uri:
'https://app.com'
,
owner:
user
)
}
let
(
:token
)
{
Doorkeeper
::
AccessToken
.
create!
(
application_id:
application
.
id
,
resource_owner_id:
user
.
id
,
scopes:
'api'
)
}
context
'passed as header'
do
it
'returns token if valid oauth_access_token'
do
env
[
'HTTP_AUTHORIZATION'
]
=
"Bearer
#{
token
.
token
}
"
expect
(
find_oauth_access_token
.
token
).
to
eq
token
.
token
end
end
context
'passed as param'
do
it
'returns user if valid oauth_access_token'
do
set_param
(
:access_token
,
token
.
token
)
expect
(
find_oauth_access_token
.
token
).
to
eq
token
.
token
end
end
it
'returns nil if no oauth_access_token'
do
expect
(
find_oauth_access_token
).
to
be_nil
end
it
'returns exception if invalid oauth_access_token'
do
env
[
'HTTP_AUTHORIZATION'
]
=
"Bearer invalid_token"
expect
{
find_oauth_access_token
}.
to
raise_error
(
Gitlab
::
Auth
::
UnauthorizedError
)
end
end
describe
'#validate_access_token!'
do
let
(
:personal_access_token
)
{
create
(
:personal_access_token
,
user:
user
)
}
it
'returns nil if no access_token present'
do
expect
(
validate_access_token!
).
to
be_nil
end
context
'token is not valid'
do
before
do
allow_any_instance_of
(
described_class
).
to
receive
(
:access_token
).
and_return
(
personal_access_token
)
end
it
'returns Gitlab::Auth::ExpiredError if token expired'
do
personal_access_token
.
expires_at
=
1
.
day
.
ago
expect
{
validate_access_token!
}.
to
raise_error
(
Gitlab
::
Auth
::
ExpiredError
)
end
it
'returns Gitlab::Auth::RevokedError if token revoked'
do
personal_access_token
.
revoke!
expect
{
validate_access_token!
}.
to
raise_error
(
Gitlab
::
Auth
::
RevokedError
)
end
it
'returns Gitlab::Auth::InsufficientScopeError if invalid token scope'
do
expect
{
validate_access_token!
(
scopes:
[
:sudo
])
}.
to
raise_error
(
Gitlab
::
Auth
::
InsufficientScopeError
)
end
end
end
end
spec/requests/api/helpers_spec.rb
View file @
eb396f71
...
...
@@ -11,7 +11,6 @@ describe API::Helpers do
let
(
:admin
)
{
create
(
:admin
)
}
let
(
:key
)
{
create
(
:key
,
user:
user
)
}
let
(
:params
)
{
{}
}
let
(
:csrf_token
)
{
SecureRandom
.
base64
(
ActionController
::
RequestForgeryProtection
::
AUTHENTICITY_TOKEN_LENGTH
)
}
let
(
:env
)
do
{
...
...
@@ -19,11 +18,14 @@ describe API::Helpers do
'rack.session'
=>
{
_csrf_token:
csrf_token
},
'REQUEST_METHOD'
=>
'GET'
'REQUEST_METHOD'
=>
'GET'
,
'CONTENT_TYPE'
=>
'text/plain;charset=utf-8'
}
end
let
(
:header
)
{
}
let
(
:route_authentication_setting
)
{
{}
}
let
(
:request
)
{
Grape
::
Request
.
new
(
env
)}
let
(
:params
)
{
request
.
params
}
before
do
allow_any_instance_of
(
self
.
class
).
to
receive
(
:options
).
and_return
({})
...
...
@@ -40,6 +42,10 @@ describe API::Helpers do
raise
Exception
.
new
(
"
#{
status
}
-
#{
message
}
"
)
end
def
set_param
(
key
,
value
)
request
.
update_param
(
key
,
value
)
end
describe
".current_user"
do
subject
{
current_user
}
...
...
@@ -135,13 +141,13 @@ describe API::Helpers do
let
(
:personal_access_token
)
{
create
(
:personal_access_token
,
user:
user
)
}
it
"returns a 401 response for an invalid token"
do
env
[
API
::
APIGuard
::
PRIVATE_TOKEN_HEADER
]
=
'invalid token'
env
[
Gitlab
::
Auth
::
UserAuthFinders
::
PRIVATE_TOKEN_HEADER
]
=
'invalid token'
expect
{
current_user
}.
to
raise_error
/401/
end
it
"returns a 403 response for a user without access"
do
env
[
API
::
APIGuard
::
PRIVATE_TOKEN_HEADER
]
=
personal_access_token
.
token
env
[
Gitlab
::
Auth
::
UserAuthFinders
::
PRIVATE_TOKEN_HEADER
]
=
personal_access_token
.
token
allow_any_instance_of
(
Gitlab
::
UserAccess
).
to
receive
(
:allowed?
).
and_return
(
false
)
expect
{
current_user
}.
to
raise_error
/403/
...
...
@@ -149,35 +155,35 @@ describe API::Helpers do
it
'returns a 403 response for a user who is blocked'
do
user
.
block!
env
[
API
::
APIGuard
::
PRIVATE_TOKEN_HEADER
]
=
personal_access_token
.
token
env
[
Gitlab
::
Auth
::
UserAuthFinders
::
PRIVATE_TOKEN_HEADER
]
=
personal_access_token
.
token
expect
{
current_user
}.
to
raise_error
/403/
end
it
"sets current_user"
do
env
[
API
::
APIGuard
::
PRIVATE_TOKEN_HEADER
]
=
personal_access_token
.
token
env
[
Gitlab
::
Auth
::
UserAuthFinders
::
PRIVATE_TOKEN_HEADER
]
=
personal_access_token
.
token
expect
(
current_user
).
to
eq
(
user
)
end
it
"does not allow tokens without the appropriate scope"
do
personal_access_token
=
create
(
:personal_access_token
,
user:
user
,
scopes:
[
'read_user'
])
env
[
API
::
APIGuard
::
PRIVATE_TOKEN_HEADER
]
=
personal_access_token
.
token
env
[
Gitlab
::
Auth
::
UserAuthFinders
::
PRIVATE_TOKEN_HEADER
]
=
personal_access_token
.
token
expect
{
current_user
}.
to
raise_error
API
::
APIGuard
::
InsufficientScopeError
expect
{
current_user
}.
to
raise_error
Gitlab
::
Auth
::
InsufficientScopeError
end
it
'does not allow revoked tokens'
do
personal_access_token
.
revoke!
env
[
API
::
APIGuard
::
PRIVATE_TOKEN_HEADER
]
=
personal_access_token
.
token
env
[
Gitlab
::
Auth
::
UserAuthFinders
::
PRIVATE_TOKEN_HEADER
]
=
personal_access_token
.
token
expect
{
current_user
}.
to
raise_error
API
::
APIGuard
::
RevokedError
expect
{
current_user
}.
to
raise_error
Gitlab
::
Auth
::
RevokedError
end
it
'does not allow expired tokens'
do
personal_access_token
.
update_attributes!
(
expires_at:
1
.
day
.
ago
)
env
[
API
::
APIGuard
::
PRIVATE_TOKEN_HEADER
]
=
personal_access_token
.
token
env
[
Gitlab
::
Auth
::
UserAuthFinders
::
PRIVATE_TOKEN_HEADER
]
=
personal_access_token
.
token
expect
{
current_user
}.
to
raise_error
API
::
APIGuard
::
ExpiredError
expect
{
current_user
}.
to
raise_error
Gitlab
::
Auth
::
ExpiredError
end
end
...
...
@@ -188,13 +194,13 @@ describe API::Helpers do
let
(
:route_authentication_setting
)
{
{
job_token_allowed:
true
}
}
it
"returns a 401 response for an invalid token"
do
env
[
API
::
APIGuard
::
JOB_TOKEN_HEADER
]
=
'invalid token'
env
[
Gitlab
::
Auth
::
UserAuthFinders
::
JOB_TOKEN_HEADER
]
=
'invalid token'
expect
{
current_user
}.
to
raise_error
/401/
end
it
"returns a 403 response for a user without access"
do
env
[
API
::
APIGuard
::
JOB_TOKEN_HEADER
]
=
job
.
token
env
[
Gitlab
::
Auth
::
UserAuthFinders
::
JOB_TOKEN_HEADER
]
=
job
.
token
allow_any_instance_of
(
Gitlab
::
UserAccess
).
to
receive
(
:allowed?
).
and_return
(
false
)
expect
{
current_user
}.
to
raise_error
/403/
...
...
@@ -202,13 +208,13 @@ describe API::Helpers do
it
'returns a 403 response for a user who is blocked'
do
user
.
block!
env
[
API
::
APIGuard
::
JOB_TOKEN_HEADER
]
=
job
.
token
env
[
Gitlab
::
Auth
::
UserAuthFinders
::
JOB_TOKEN_HEADER
]
=
job
.
token
expect
{
current_user
}.
to
raise_error
/403/
end
it
"sets current_user"
do
env
[
API
::
APIGuard
::
JOB_TOKEN_HEADER
]
=
job
.
token
env
[
Gitlab
::
Auth
::
UserAuthFinders
::
JOB_TOKEN_HEADER
]
=
job
.
token
expect
(
current_user
).
to
eq
(
user
)
end
...
...
@@ -218,7 +224,7 @@ describe API::Helpers do
let
(
:route_authentication_setting
)
{
{
job_token_allowed:
false
}
}
it
"sets current_user to nil"
do
env
[
API
::
APIGuard
::
JOB_TOKEN_HEADER
]
=
job
.
token
env
[
Gitlab
::
Auth
::
UserAuthFinders
::
JOB_TOKEN_HEADER
]
=
job
.
token
allow_any_instance_of
(
Gitlab
::
UserAccess
).
to
receive
(
:allowed?
).
and_return
(
true
)
expect
(
current_user
).
to
be_nil
...
...
@@ -398,7 +404,7 @@ describe API::Helpers do
context
'when using param'
do
context
'when providing username'
do
before
do
params
[
API
::
Helpers
::
SUDO_PARAM
]
=
user
.
username
set_param
(
API
::
Helpers
::
SUDO_PARAM
,
user
.
username
)
end
it_behaves_like
'successful sudo'
...
...
@@ -406,7 +412,7 @@ describe API::Helpers do
context
'when providing user ID'
do
before
do
params
[
API
::
Helpers
::
SUDO_PARAM
]
=
user
.
id
.
to_s
set_param
(
API
::
Helpers
::
SUDO_PARAM
,
user
.
id
.
to_s
)
end
it_behaves_like
'successful sudo'
...
...
@@ -416,7 +422,7 @@ describe API::Helpers do
context
'when user does not exist'
do
before
do
params
[
API
::
Helpers
::
SUDO_PARAM
]
=
'nonexistent'
set_param
(
API
::
Helpers
::
SUDO_PARAM
,
'nonexistent'
)
end
it
'raises an error'
do
...
...
@@ -430,11 +436,11 @@ describe API::Helpers do
token
.
scopes
=
%w[api]
token
.
save!
params
[
API
::
Helpers
::
SUDO_PARAM
]
=
user
.
id
.
to_s
set_param
(
API
::
Helpers
::
SUDO_PARAM
,
user
.
id
.
to_s
)
end
it
'raises an error'
do
expect
{
current_user
}.
to
raise_error
API
::
APIGuard
::
InsufficientScopeError
expect
{
current_user
}.
to
raise_error
Gitlab
::
Auth
::
InsufficientScopeError
end
end
end
...
...
@@ -444,7 +450,7 @@ describe API::Helpers do
token
.
user
=
user
token
.
save!
params
[
API
::
Helpers
::
SUDO_PARAM
]
=
user
.
id
.
to_s
set_param
(
API
::
Helpers
::
SUDO_PARAM
,
user
.
id
.
to_s
)
end
it
'raises an error'
do
...
...
@@ -468,7 +474,7 @@ describe API::Helpers do
context
'passed as param'
do
before
do
params
[
API
::
APIGuard
::
PRIVATE_TOKEN_PARAM
]
=
token
.
token
set_param
(
Gitlab
::
Auth
::
UserAuthFinders
::
PRIVATE_TOKEN_PARAM
,
token
.
token
)
end
it_behaves_like
'sudo'
...
...
@@ -476,7 +482,7 @@ describe API::Helpers do
context
'passed as header'
do
before
do
env
[
API
::
APIGuard
::
PRIVATE_TOKEN_HEADER
]
=
token
.
token
env
[
Gitlab
::
Auth
::
UserAuthFinders
::
PRIVATE_TOKEN_HEADER
]
=
token
.
token
end
it_behaves_like
'sudo'
...
...
spec/requests/rack_attack_global_spec.rb
0 → 100644
View file @
eb396f71
require
'spec_helper'
describe
'Rack Attack global throttles'
do
let
(
:settings
)
{
Gitlab
::
CurrentSettings
.
current_application_settings
}
# Start with really high limits and override them with low limits to ensure
# the right settings are being exercised
let
(
:settings_to_set
)
do
{
throttle_unauthenticated_requests_per_period:
100
,
throttle_unauthenticated_period_in_seconds:
1
,
throttle_authenticated_api_requests_per_period:
100
,
throttle_authenticated_api_period_in_seconds:
1
,
throttle_authenticated_web_requests_per_period:
100
,
throttle_authenticated_web_period_in_seconds:
1
}
end
let
(
:requests_per_period
)
{
1
}
let
(
:period_in_seconds
)
{
10000
}
let
(
:period
)
{
period_in_seconds
.
seconds
}
let
(
:url_that_does_not_require_authentication
)
{
'/users/sign_in'
}
let
(
:url_that_requires_authentication
)
{
'/dashboard/snippets'
}
let
(
:api_partial_url
)
{
'/todos'
}
around
do
|
example
|
# Instead of test environment's :null_store so the throttles can increment
Rack
::
Attack
.
cache
.
store
=
ActiveSupport
::
Cache
::
MemoryStore
.
new
# Make time-dependent tests deterministic
Timecop
.
freeze
{
example
.
run
}
Rack
::
Attack
.
cache
.
store
=
Rails
.
cache
end
# Requires let variables:
# * throttle_setting_prefix (e.g. "throttle_authenticated_api" or "throttle_authenticated_web")
# * get_args
# * other_user_get_args
shared_examples_for
'rate-limited token-authenticated requests'
do
before
do
# Set low limits
settings_to_set
[
:"
#{
throttle_setting_prefix
}
_requests_per_period"
]
=
requests_per_period
settings_to_set
[
:"
#{
throttle_setting_prefix
}
_period_in_seconds"
]
=
period_in_seconds
end
context
'when the throttle is enabled'
do
before
do
settings_to_set
[
:"
#{
throttle_setting_prefix
}
_enabled"
]
=
true
stub_application_setting
(
settings_to_set
)
end
it
'rejects requests over the rate limit'
do
# At first, allow requests under the rate limit.
requests_per_period
.
times
do
get
(
*
get_args
)
expect
(
response
).
to
have_http_status
200
end
# the last straw
expect_rejection
{
get
(
*
get_args
)
}
end
it
'allows requests after throttling and then waiting for the next period'
do
requests_per_period
.
times
do
get
(
*
get_args
)
expect
(
response
).
to
have_http_status
200
end
expect_rejection
{
get
(
*
get_args
)
}
Timecop
.
travel
(
period
.
from_now
)
do
requests_per_period
.
times
do
get
(
*
get_args
)
expect
(
response
).
to
have_http_status
200
end
expect_rejection
{
get
(
*
get_args
)
}
end
end
it
'counts requests from different users separately, even from the same IP'
do
requests_per_period
.
times
do
get
(
*
get_args
)
expect
(
response
).
to
have_http_status
200
end
# would be over the limit if this wasn't a different user
get
(
*
other_user_get_args
)
expect
(
response
).
to
have_http_status
200
end
it
'counts all requests from the same user, even via different IPs'
do
requests_per_period
.
times
do
get
(
*
get_args
)
expect
(
response
).
to
have_http_status
200
end
expect_any_instance_of
(
Rack
::
Attack
::
Request
).
to
receive
(
:ip
).
and_return
(
'1.2.3.4'
)
expect_rejection
{
get
(
*
get_args
)
}
end
end
context
'when the throttle is disabled'
do
before
do
settings_to_set
[
:"
#{
throttle_setting_prefix
}
_enabled"
]
=
false
stub_application_setting
(
settings_to_set
)
end
it
'allows requests over the rate limit'
do
(
1
+
requests_per_period
).
times
do
get
(
*
get_args
)
expect
(
response
).
to
have_http_status
200
end
end
end
end
describe
'unauthenticated requests'
do
before
do
# Set low limits
settings_to_set
[
:throttle_unauthenticated_requests_per_period
]
=
requests_per_period
settings_to_set
[
:throttle_unauthenticated_period_in_seconds
]
=
period_in_seconds
end
context
'when the throttle is enabled'
do
before
do
settings_to_set
[
:throttle_unauthenticated_enabled
]
=
true
stub_application_setting
(
settings_to_set
)
end
it
'rejects requests over the rate limit'
do
# At first, allow requests under the rate limit.
requests_per_period
.
times
do
get
url_that_does_not_require_authentication
expect
(
response
).
to
have_http_status
200
end
# the last straw
expect_rejection
{
get
url_that_does_not_require_authentication
}
end
it
'allows requests after throttling and then waiting for the next period'
do
requests_per_period
.
times
do
get
url_that_does_not_require_authentication
expect
(
response
).
to
have_http_status
200
end
expect_rejection
{
get
url_that_does_not_require_authentication
}
Timecop
.
travel
(
period
.
from_now
)
do
requests_per_period
.
times
do
get
url_that_does_not_require_authentication
expect
(
response
).
to
have_http_status
200
end
expect_rejection
{
get
url_that_does_not_require_authentication
}
end
end
it
'counts requests from different IPs separately'
do
requests_per_period
.
times
do
get
url_that_does_not_require_authentication
expect
(
response
).
to
have_http_status
200
end
expect_any_instance_of
(
Rack
::
Attack
::
Request
).
to
receive
(
:ip
).
and_return
(
'1.2.3.4'
)
# would be over limit for the same IP
get
url_that_does_not_require_authentication
expect
(
response
).
to
have_http_status
200
end
end
context
'when the throttle is disabled'
do
before
do
settings_to_set
[
:throttle_unauthenticated_enabled
]
=
false
stub_application_setting
(
settings_to_set
)
end
it
'allows requests over the rate limit'
do
(
1
+
requests_per_period
).
times
do
get
url_that_does_not_require_authentication
expect
(
response
).
to
have_http_status
200
end
end
end
end
describe
'API requests authenticated with personal access token'
,
:api
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:token
)
{
create
(
:personal_access_token
,
user:
user
)
}
let
(
:other_user
)
{
create
(
:user
)
}
let
(
:other_user_token
)
{
create
(
:personal_access_token
,
user:
other_user
)
}
let
(
:throttle_setting_prefix
)
{
'throttle_authenticated_api'
}
context
'with the token in the query string'
do
let
(
:get_args
)
{
[
api
(
api_partial_url
,
personal_access_token:
token
)]
}
let
(
:other_user_get_args
)
{
[
api
(
api_partial_url
,
personal_access_token:
other_user_token
)]
}
it_behaves_like
'rate-limited token-authenticated requests'
end
context
'with the token in the headers'
do
let
(
:get_args
)
{
api_get_args_with_token_headers
(
api_partial_url
,
personal_access_token_headers
(
token
))
}
let
(
:other_user_get_args
)
{
api_get_args_with_token_headers
(
api_partial_url
,
personal_access_token_headers
(
other_user_token
))
}
it_behaves_like
'rate-limited token-authenticated requests'
end
end
describe
'API requests authenticated with OAuth token'
,
:api
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:application
)
{
Doorkeeper
::
Application
.
create!
(
name:
"MyApp"
,
redirect_uri:
"https://app.com"
,
owner:
user
)
}
let
(
:token
)
{
Doorkeeper
::
AccessToken
.
create!
(
application_id:
application
.
id
,
resource_owner_id:
user
.
id
,
scopes:
"api"
)
}
let
(
:other_user
)
{
create
(
:user
)
}
let
(
:other_user_application
)
{
Doorkeeper
::
Application
.
create!
(
name:
"MyApp"
,
redirect_uri:
"https://app.com"
,
owner:
other_user
)
}
let
(
:other_user_token
)
{
Doorkeeper
::
AccessToken
.
create!
(
application_id:
application
.
id
,
resource_owner_id:
other_user
.
id
,
scopes:
"api"
)
}
let
(
:throttle_setting_prefix
)
{
'throttle_authenticated_api'
}
context
'with the token in the query string'
do
let
(
:get_args
)
{
[
api
(
api_partial_url
,
oauth_access_token:
token
)]
}
let
(
:other_user_get_args
)
{
[
api
(
api_partial_url
,
oauth_access_token:
other_user_token
)]
}
it_behaves_like
'rate-limited token-authenticated requests'
end
context
'with the token in the headers'
do
let
(
:get_args
)
{
api_get_args_with_token_headers
(
api_partial_url
,
oauth_token_headers
(
token
))
}
let
(
:other_user_get_args
)
{
api_get_args_with_token_headers
(
api_partial_url
,
oauth_token_headers
(
other_user_token
))
}
it_behaves_like
'rate-limited token-authenticated requests'
end
end
describe
'"web" (non-API) requests authenticated with RSS token'
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:other_user
)
{
create
(
:user
)
}
let
(
:throttle_setting_prefix
)
{
'throttle_authenticated_web'
}
context
'with the token in the query string'
do
let
(
:get_args
)
{
[
rss_url
(
user
),
nil
]
}
let
(
:other_user_get_args
)
{
[
rss_url
(
other_user
),
nil
]
}
it_behaves_like
'rate-limited token-authenticated requests'
end
end
describe
'web requests authenticated with regular login'
do
let
(
:user
)
{
create
(
:user
)
}
before
do
login_as
(
user
)
# Set low limits
settings_to_set
[
:throttle_authenticated_web_requests_per_period
]
=
requests_per_period
settings_to_set
[
:throttle_authenticated_web_period_in_seconds
]
=
period_in_seconds
end
context
'when the throttle is enabled'
do
before
do
settings_to_set
[
:throttle_authenticated_web_enabled
]
=
true
stub_application_setting
(
settings_to_set
)
end
it
'rejects requests over the rate limit'
do
# At first, allow requests under the rate limit.
requests_per_period
.
times
do
get
url_that_requires_authentication
expect
(
response
).
to
have_http_status
200
end
# the last straw
expect_rejection
{
get
url_that_requires_authentication
}
end
it
'allows requests after throttling and then waiting for the next period'
do
requests_per_period
.
times
do
get
url_that_requires_authentication
expect
(
response
).
to
have_http_status
200
end
expect_rejection
{
get
url_that_requires_authentication
}
Timecop
.
travel
(
period
.
from_now
)
do
requests_per_period
.
times
do
get
url_that_requires_authentication
expect
(
response
).
to
have_http_status
200
end
expect_rejection
{
get
url_that_requires_authentication
}
end
end
it
'counts requests from different users separately, even from the same IP'
do
requests_per_period
.
times
do
get
url_that_requires_authentication
expect
(
response
).
to
have_http_status
200
end
# would be over the limit if this wasn't a different user
login_as
(
create
(
:user
))
get
url_that_requires_authentication
expect
(
response
).
to
have_http_status
200
end
it
'counts all requests from the same user, even via different IPs'
do
requests_per_period
.
times
do
get
url_that_requires_authentication
expect
(
response
).
to
have_http_status
200
end
expect_any_instance_of
(
Rack
::
Attack
::
Request
).
to
receive
(
:ip
).
and_return
(
'1.2.3.4'
)
expect_rejection
{
get
url_that_requires_authentication
}
end
end
context
'when the throttle is disabled'
do
before
do
settings_to_set
[
:throttle_authenticated_web_enabled
]
=
false
stub_application_setting
(
settings_to_set
)
end
it
'allows requests over the rate limit'
do
(
1
+
requests_per_period
).
times
do
get
url_that_requires_authentication
expect
(
response
).
to
have_http_status
200
end
end
end
end
def
api_get_args_with_token_headers
(
partial_url
,
token_headers
)
[
"/api/
#{
API
::
API
.
version
}#{
partial_url
}
"
,
nil
,
token_headers
]
end
def
rss_url
(
user
)
"/dashboard/projects.atom?rss_token=
#{
user
.
rss_token
}
"
end
def
private_token_headers
(
user
)
{
'HTTP_PRIVATE_TOKEN'
=>
user
.
private_token
}
end
def
personal_access_token_headers
(
personal_access_token
)
{
'HTTP_PRIVATE_TOKEN'
=>
personal_access_token
.
token
}
end
def
oauth_token_headers
(
oauth_access_token
)
{
'AUTHORIZATION'
=>
"Bearer
#{
oauth_access_token
.
token
}
"
}
end
def
expect_rejection
(
&
block
)
yield
expect
(
response
).
to
have_http_status
(
429
)
end
end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment