2017-12-10 19:07:22 -05:00
# frozen_string_literal: true
2020-10-06 12:20:15 -04:00
require_relative '../route_matcher'
2017-12-10 19:07:22 -05:00
2013-10-09 00:10:37 -04:00
class Auth :: DefaultCurrentUserProvider
2018-02-18 18:12:51 -05:00
CURRENT_USER_KEY || = " _DISCOURSE_CURRENT_USER "
API_KEY || = " api_key "
2019-03-08 11:13:31 -05:00
API_USERNAME || = " api_username "
HEADER_API_KEY || = " HTTP_API_KEY "
HEADER_API_USERNAME || = " HTTP_API_USERNAME "
HEADER_API_USER_EXTERNAL_ID || = " HTTP_API_USER_EXTERNAL_ID "
HEADER_API_USER_ID || = " HTTP_API_USER_ID "
2020-05-12 08:35:36 -04:00
PARAMETER_USER_API_KEY || = " user_api_key "
2018-02-18 18:12:51 -05:00
USER_API_KEY || = " HTTP_USER_API_KEY "
USER_API_CLIENT_ID || = " HTTP_USER_API_CLIENT_ID "
API_KEY_ENV || = " _DISCOURSE_API "
USER_API_KEY_ENV || = " _DISCOURSE_USER_API "
2018-03-13 16:48:40 -04:00
TOKEN_COOKIE || = ENV [ 'DISCOURSE_TOKEN_COOKIE' ] || " _t "
2018-02-18 18:12:51 -05:00
PATH_INFO || = " PATH_INFO "
2016-07-27 22:58:49 -04:00
COOKIE_ATTEMPTS_PER_MIN || = 10
2018-03-06 00:49:31 -05:00
BAD_TOKEN || = " _DISCOURSE_BAD_TOKEN "
2013-10-09 00:10:37 -04:00
2020-05-13 07:54:28 -04:00
PARAMETER_API_PATTERNS || = [
2020-10-06 12:20:15 -04:00
RouteMatcher . new (
methods : :get ,
actions : [
2020-05-12 08:35:36 -04:00
" posts # latest " ,
" posts # user_posts_feed " ,
" groups # posts_feed " ,
" groups # mentions_feed " ,
2020-05-12 11:08:35 -04:00
" list # user_topics_feed " ,
2020-05-12 08:35:36 -04:00
" list # category_feed " ,
2020-05-12 11:08:35 -04:00
" topics # feed " ,
" badges # show " ,
" tags # tag_feed " ,
" tags # show " ,
* [ :latest , :unread , :new , :read , :posted , :bookmarks ] . map { | f | " list # #{ f } _feed " } ,
* [ :all , :yearly , :quarterly , :monthly , :weekly , :daily ] . map { | p | " list # top_ #{ p } _feed " } ,
* [ :latest , :unread , :new , :read , :posted , :bookmarks ] . map { | f | " tags # show_ #{ f } " }
2020-05-12 08:35:36 -04:00
] ,
2020-10-06 12:20:15 -04:00
formats : :rss
) ,
RouteMatcher . new (
methods : :get ,
actions : " users # bookmarks " ,
formats : :ics
) ,
RouteMatcher . new (
methods : :post ,
actions : " admin/email # handle_mail " ,
formats : nil
2021-05-05 22:59:52 -04:00
) ,
2020-05-12 08:35:36 -04:00
]
2013-10-09 00:10:37 -04:00
# do all current user initialization here
2018-09-04 02:17:05 -04:00
def initialize ( env )
2013-10-09 00:10:37 -04:00
@env = env
@request = Rack :: Request . new ( env )
end
# our current user, return nil if none is found
def current_user
return @env [ CURRENT_USER_KEY ] if @env . key? ( CURRENT_USER_KEY )
2014-10-23 22:38:00 -04:00
# bypass if we have the shared session header
if shared_key = @env [ 'HTTP_X_SHARED_SESSION_KEY' ]
2019-12-03 04:05:53 -05:00
uid = Discourse . redis . get ( " shared_session_key_ #{ shared_key } " )
2014-10-23 22:38:00 -04:00
user = nil
if uid
user = User . find_by ( id : uid . to_i )
end
@env [ CURRENT_USER_KEY ] = user
return user
end
2014-05-22 18:13:25 -04:00
request = @request
2013-10-09 00:10:37 -04:00
2017-02-17 11:02:33 -05:00
user_api_key = @env [ USER_API_KEY ]
2020-05-12 08:35:36 -04:00
api_key = @env [ HEADER_API_KEY ]
if ! @env . blank? && request [ PARAMETER_USER_API_KEY ] && api_parameter_allowed?
user_api_key || = request [ PARAMETER_USER_API_KEY ]
end
if ! @env . blank? && request [ API_KEY ] && api_parameter_allowed?
api_key || = request [ API_KEY ]
end
2017-02-17 11:02:33 -05:00
auth_token = request . cookies [ TOKEN_COOKIE ] unless user_api_key || api_key
2013-10-09 00:10:37 -04:00
current_user = nil
if auth_token && auth_token . length == 32
2016-08-08 20:02:18 -04:00
limiter = RateLimiter . new ( nil , " cookie_auth_ #{ request . ip } " , COOKIE_ATTEMPTS_PER_MIN , 60 )
2018-02-09 19:09:54 -05:00
if limiter . can_perform?
2020-06-03 04:36:51 -04:00
@user_token = begin
UserAuthToken . lookup (
auth_token ,
seen : true ,
user_agent : @env [ 'HTTP_USER_AGENT' ] ,
path : @env [ 'REQUEST_PATH' ] ,
client_ip : @request . ip
)
rescue ActiveRecord :: ReadOnlyError
nil
end
2017-02-13 14:01:01 -05:00
2017-01-31 17:21:37 -05:00
current_user = @user_token . try ( :user )
2016-08-08 20:02:18 -04:00
end
2016-07-27 22:58:49 -04:00
2018-03-06 00:49:31 -05:00
if ! current_user
@env [ BAD_TOKEN ] = true
2016-07-27 22:58:49 -04:00
begin
2018-09-04 02:17:05 -04:00
limiter . performed!
2016-07-27 22:58:49 -04:00
rescue RateLimiter :: LimitExceeded
2018-02-09 19:09:54 -05:00
raise Discourse :: InvalidAccess . new (
'Invalid Access' ,
nil ,
delete_cookie : TOKEN_COOKIE
)
2016-07-27 22:58:49 -04:00
end
end
2018-03-06 00:49:31 -05:00
elsif @env [ 'HTTP_DISCOURSE_LOGGED_IN' ]
@env [ BAD_TOKEN ] = true
2013-10-09 00:10:37 -04:00
end
# possible we have an api call, impersonate
2017-02-17 11:02:33 -05:00
if api_key
2014-05-22 18:13:25 -04:00
current_user = lookup_api_user ( api_key , request )
2017-10-20 10:30:13 -04:00
raise Discourse :: InvalidAccess . new ( I18n . t ( 'invalid_api_credentials' ) , nil , custom_message : " invalid_api_credentials " ) unless current_user
2017-02-17 11:02:33 -05:00
raise Discourse :: InvalidAccess if current_user . suspended? || ! current_user . active
2014-05-22 18:13:25 -04:00
@env [ API_KEY_ENV ] = true
2021-06-03 05:52:43 -04:00
rate_limit_admin_api_requests!
2013-10-09 00:10:37 -04:00
end
2016-08-15 03:58:33 -04:00
# user api key handling
2017-02-17 11:02:33 -05:00
if user_api_key
2016-08-15 03:58:33 -04:00
2021-10-21 12:43:26 -04:00
hashed_user_api_key = ApiKey . hash_key ( user_api_key )
limiter_min = RateLimiter . new ( nil , " user_api_min_ #{ hashed_user_api_key } " , GlobalSetting . max_user_api_reqs_per_minute , 60 )
limiter_day = RateLimiter . new ( nil , " user_api_day_ #{ hashed_user_api_key } " , GlobalSetting . max_user_api_reqs_per_day , 86400 )
2016-08-15 03:58:33 -04:00
unless limiter_day . can_perform?
2018-09-04 02:17:05 -04:00
limiter_day . performed!
2016-08-15 03:58:33 -04:00
end
unless limiter_min . can_perform?
2018-09-04 02:17:05 -04:00
limiter_min . performed!
2016-08-15 03:58:33 -04:00
end
2017-02-17 11:02:33 -05:00
current_user = lookup_user_api_user_and_update_key ( user_api_key , @env [ USER_API_CLIENT_ID ] )
2016-08-15 03:58:33 -04:00
raise Discourse :: InvalidAccess unless current_user
2017-02-17 11:02:33 -05:00
raise Discourse :: InvalidAccess if current_user . suspended? || ! current_user . active
2016-08-15 03:58:33 -04:00
2018-09-04 02:17:05 -04:00
limiter_min . performed!
limiter_day . performed!
2016-08-15 03:58:33 -04:00
2016-12-15 20:05:20 -05:00
@env [ USER_API_KEY_ENV ] = true
2016-08-15 03:58:33 -04:00
end
2017-02-17 11:02:33 -05:00
# keep this rule here as a safeguard
# under no conditions to suspended or inactive accounts get current_user
if current_user && ( current_user . suspended? || ! current_user . active )
current_user = nil
end
2018-07-18 11:04:57 -04:00
if current_user && should_update_last_seen?
u = current_user
2020-03-11 02:42:56 -04:00
ip = request . ip
2018-07-18 11:04:57 -04:00
Scheduler :: Defer . later " Updating Last Seen " do
u . update_last_seen!
2020-03-11 02:42:56 -04:00
u . update_ip_address! ( ip )
2018-07-18 11:04:57 -04:00
end
end
2013-10-09 00:10:37 -04:00
@env [ CURRENT_USER_KEY ] = current_user
end
2016-07-24 22:07:31 -04:00
def refresh_session ( user , session , cookies )
2017-01-31 17:21:37 -05:00
# if user was not loaded, no point refreshing session
# it could be an anonymous path, this would add cost
return if is_api? || ! @env . key? ( CURRENT_USER_KEY )
2017-02-17 11:02:33 -05:00
if ! is_user_api? && @user_token && @user_token . user == user
2017-01-31 17:21:37 -05:00
rotated_at = @user_token . rotated_at
needs_rotation = @user_token . auth_token_seen ? rotated_at < UserAuthToken :: ROTATE_TIME . ago : rotated_at < UserAuthToken :: URGENT_ROTATE_TIME . ago
2018-05-03 21:11:44 -04:00
if needs_rotation
2017-01-31 17:21:37 -05:00
if @user_token . rotate! ( user_agent : @env [ 'HTTP_USER_AGENT' ] ,
2017-03-07 13:27:34 -05:00
client_ip : @request . ip ,
path : @env [ 'REQUEST_PATH' ] )
2017-01-31 17:21:37 -05:00
cookies [ TOKEN_COOKIE ] = cookie_hash ( @user_token . unhashed_auth_token )
2020-04-14 12:32:24 -04:00
DiscourseEvent . trigger ( :user_session_refreshed , user )
2017-01-31 17:21:37 -05:00
end
end
2016-07-24 22:07:31 -04:00
end
2017-01-31 17:21:37 -05:00
2016-07-27 22:58:49 -04:00
if ! user && cookies . key? ( TOKEN_COOKIE )
2017-03-07 13:27:34 -05:00
cookies . delete ( TOKEN_COOKIE )
2016-07-27 22:58:49 -04:00
end
2016-07-24 22:07:31 -04:00
end
2018-11-12 09:34:12 -05:00
def log_on_user ( user , session , cookies , opts = { } )
2018-10-25 18:29:28 -04:00
@user_token = UserAuthToken . generate! (
user_id : user . id ,
user_agent : @env [ 'HTTP_USER_AGENT' ] ,
path : @env [ 'REQUEST_PATH' ] ,
client_ip : @request . ip ,
2018-11-12 09:34:12 -05:00
staff : user . staff? ,
2018-11-12 10:00:12 -05:00
impersonate : opts [ :impersonate ] )
2016-07-25 21:37:41 -04:00
2017-01-31 17:21:37 -05:00
cookies [ TOKEN_COOKIE ] = cookie_hash ( @user_token . unhashed_auth_token )
2020-03-17 11:48:24 -04:00
user . unstage!
2013-11-01 19:25:43 -04:00
make_developer_admin ( user )
2016-04-26 13:08:19 -04:00
enable_bootstrap_mode ( user )
2019-11-27 07:39:31 -05:00
UserAuthToken . enforce_session_count_limit! ( user . id )
2013-10-09 00:10:37 -04:00
@env [ CURRENT_USER_KEY ] = user
end
2017-01-31 17:21:37 -05:00
def cookie_hash ( unhashed_auth_token )
2017-02-23 12:01:28 -05:00
hash = {
2017-01-31 17:21:37 -05:00
value : unhashed_auth_token ,
2016-10-16 21:11:15 -04:00
httponly : true ,
2017-06-21 16:18:24 -04:00
secure : SiteSetting . force_https
2016-10-16 21:11:15 -04:00
}
2017-02-23 12:01:28 -05:00
2020-09-11 01:11:13 -04:00
if SiteSetting . persistent_sessions
hash [ :expires ] = SiteSetting . maximum_session_age . hours . from_now
end
2017-02-23 12:01:28 -05:00
if SiteSetting . same_site_cookies != " Disabled "
hash [ :same_site ] = SiteSetting . same_site_cookies
end
hash
2016-10-16 21:11:15 -04:00
end
2013-11-01 19:25:43 -04:00
def make_developer_admin ( user )
if user . active? &&
! user . admin &&
Rails . configuration . respond_to? ( :developer_emails ) &&
Rails . configuration . developer_emails . include? ( user . email )
2014-03-24 03:03:39 -04:00
user . admin = true
user . save
2013-11-01 19:25:43 -04:00
end
end
2016-04-26 13:08:19 -04:00
def enable_bootstrap_mode ( user )
2018-05-13 11:00:02 -04:00
return if SiteSetting . bootstrap_mode_enabled
if user . admin && user . last_seen_at . nil? && user . is_singular_admin?
Jobs . enqueue ( :enable_bootstrap_mode , user_id : user . id )
end
2016-04-26 13:08:19 -04:00
end
2013-10-09 00:10:37 -04:00
def log_off_user ( session , cookies )
2017-01-31 17:21:37 -05:00
user = current_user
2018-05-13 11:00:02 -04:00
2017-01-31 17:21:37 -05:00
if SiteSetting . log_out_strict && user
user . user_auth_tokens . destroy_all
2016-05-18 03:27:54 -04:00
if user . admin && defined? ( Rack :: MiniProfiler )
# clear the profiling cookie to keep stuff tidy
2016-07-27 22:58:49 -04:00
cookies . delete ( " __profilin " )
2016-05-18 03:27:54 -04:00
end
2016-07-04 05:20:30 -04:00
user . logged_out
2017-01-31 17:21:37 -05:00
elsif user && @user_token
@user_token . destroy
2015-01-27 20:56:25 -05:00
end
2017-08-31 00:06:56 -04:00
2019-03-19 08:39:13 -04:00
cookies . delete ( 'authentication_data' )
2016-07-27 22:58:49 -04:00
cookies . delete ( TOKEN_COOKIE )
2013-10-09 00:10:37 -04:00
end
# api has special rights return true if api was detected
def is_api?
current_user
2016-12-15 20:05:20 -05:00
! ! ( @env [ API_KEY_ENV ] )
end
def is_user_api?
current_user
! ! ( @env [ USER_API_KEY_ENV ] )
2013-10-09 00:10:37 -04:00
end
def has_auth_cookie?
2014-05-22 18:13:25 -04:00
cookie = @request . cookies [ TOKEN_COOKIE ]
2013-10-09 00:10:37 -04:00
! cookie . nil? && cookie . length == 32
end
2014-05-22 18:13:25 -04:00
def should_update_last_seen?
2020-07-21 01:43:28 -04:00
return false unless can_write?
2019-01-22 05:07:48 -05:00
2019-04-15 12:34:34 -04:00
api = ! ! ( @env [ API_KEY_ENV ] ) || ! ! ( @env [ USER_API_KEY_ENV ] )
if @request . xhr? || api
2020-03-26 02:35:32 -04:00
@env [ " HTTP_DISCOURSE_PRESENT " ] == " true "
2017-02-28 12:34:57 -05:00
else
true
end
2014-05-22 18:13:25 -04:00
end
protected
2016-10-14 01:05:27 -04:00
def lookup_user_api_user_and_update_key ( user_api_key , client_id )
2020-09-29 05:57:48 -04:00
if api_key = UserApiKey . active . with_key ( user_api_key ) . includes ( :user , :scopes ) . first
2016-10-14 01:05:27 -04:00
unless api_key . allow? ( @env )
raise Discourse :: InvalidAccess
end
2020-07-21 01:43:28 -04:00
if can_write?
api_key . update_columns ( last_used_at : Time . zone . now )
2018-08-21 22:56:49 -04:00
2020-07-21 01:43:28 -04:00
if client_id . present? && client_id != api_key . client_id
2018-08-21 22:56:49 -04:00
2020-07-21 01:43:28 -04:00
# invalidate old dupe api key for client if needed
UserApiKey
. where ( client_id : client_id , user_id : api_key . user_id )
. where ( 'id <> ?' , api_key . id )
. destroy_all
2018-08-21 22:56:49 -04:00
2020-07-21 01:43:28 -04:00
api_key . update_columns ( client_id : client_id )
end
2016-08-15 03:58:33 -04:00
end
api_key . user
end
end
2014-05-22 18:13:25 -04:00
def lookup_api_user ( api_key_value , request )
2019-12-12 06:45:00 -05:00
if api_key = ApiKey . active . with_key ( api_key_value ) . includes ( :user ) . first
2019-03-12 19:16:42 -04:00
api_username = header_api_key? ? @env [ HEADER_API_USERNAME ] : request [ API_USERNAME ]
2019-11-08 19:28:48 -05:00
2020-10-06 12:20:15 -04:00
unless api_key . request_allowed? ( @env )
2016-06-01 15:48:06 -04:00
Rails . logger . warn ( " [Unauthorized API Access] username: #{ api_username } , IP address: #{ request . ip } " )
2014-11-19 23:21:49 -05:00
return nil
end
2019-09-03 04:10:29 -04:00
user =
if api_key . user
api_key . user if ! api_username || ( api_key . user . username_lower == api_username . downcase )
elsif api_username
User . find_by ( username_lower : api_username . downcase )
elsif user_id = header_api_key? ? @env [ HEADER_API_USER_ID ] : request [ " api_user_id " ]
User . find_by ( id : user_id . to_i )
elsif external_id = header_api_key? ? @env [ HEADER_API_USER_EXTERNAL_ID ] : request [ " api_user_external_id " ]
SingleSignOnRecord . find_by ( external_id : external_id . to_s ) . try ( :user )
end
2020-07-21 01:43:28 -04:00
if user && can_write?
2019-09-03 04:10:29 -04:00
api_key . update_columns ( last_used_at : Time . zone . now )
2014-05-22 18:13:25 -04:00
end
2019-09-03 04:10:29 -04:00
user
2014-05-22 18:13:25 -04:00
end
end
2018-09-04 04:35:49 -04:00
private
2020-08-24 05:24:52 -04:00
def parameter_api_patterns
PARAMETER_API_PATTERNS + DiscoursePluginRegistry . api_parameter_routes
end
2020-05-12 08:35:36 -04:00
# By default we only allow headers for sending API credentials
# However, in some scenarios it is essential to send them via url parameters
# so we need to add some exceptions
def api_parameter_allowed?
2020-10-06 12:20:15 -04:00
parameter_api_patterns . any? { | p | p . match? ( env : @env ) }
2020-03-16 14:05:24 -04:00
end
2019-03-12 19:16:42 -04:00
def header_api_key?
! ! @env [ HEADER_API_KEY ]
end
2021-06-03 05:52:43 -04:00
def rate_limit_admin_api_requests!
2018-09-04 04:35:49 -04:00
return if Rails . env == " profile "
2021-06-03 05:52:43 -04:00
limit = GlobalSetting . max_admin_api_reqs_per_minute . to_i
if GlobalSetting . respond_to? ( :max_admin_api_reqs_per_key_per_minute )
2021-11-12 09:52:59 -05:00
Discourse . deprecate ( " DISCOURSE_MAX_ADMIN_API_REQS_PER_KEY_PER_MINUTE is deprecated. Please use DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE " , drop_from : '2.9.0' )
2021-06-03 05:52:43 -04:00
limit = [ GlobalSetting . max_admin_api_reqs_per_key_per_minute . to_i , limit ] . max
end
global_limit = RateLimiter . new (
2018-09-04 04:35:49 -04:00
nil ,
2021-06-03 05:52:43 -04:00
" admin_api_min " ,
limit ,
2018-09-04 04:35:49 -04:00
60
2021-06-03 05:52:43 -04:00
)
global_limit . performed!
2018-09-04 04:35:49 -04:00
end
2020-07-21 01:43:28 -04:00
def can_write?
@can_write || = ! Discourse . pg_readonly_mode?
end
2013-10-09 00:10:37 -04:00
end