2017-12-11 08:07:22 +08:00
# frozen_string_literal: true
2020-10-07 00:20:15 +08:00
require_relative '../route_matcher'
2017-12-11 08:07:22 +08:00
2013-10-09 12:10:37 +08:00
class Auth :: DefaultCurrentUserProvider
2018-02-19 07:12:51 +08:00
CURRENT_USER_KEY || = " _DISCOURSE_CURRENT_USER "
API_KEY || = " api_key "
2019-03-09 00:13:31 +08: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 20:35:36 +08:00
PARAMETER_USER_API_KEY || = " user_api_key "
2018-02-19 07:12:51 +08: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-14 04:48:40 +08:00
TOKEN_COOKIE || = ENV [ 'DISCOURSE_TOKEN_COOKIE' ] || " _t "
2018-02-19 07:12:51 +08:00
PATH_INFO || = " PATH_INFO "
2016-07-28 10:58:49 +08:00
COOKIE_ATTEMPTS_PER_MIN || = 10
2018-03-06 13:49:31 +08:00
BAD_TOKEN || = " _DISCOURSE_BAD_TOKEN "
2013-10-09 12:10:37 +08:00
2020-05-13 19:54:28 +08:00
PARAMETER_API_PATTERNS || = [
2020-10-07 00:20:15 +08:00
RouteMatcher . new (
methods : :get ,
actions : [
2020-05-12 20:35:36 +08:00
" posts # latest " ,
" posts # user_posts_feed " ,
" groups # posts_feed " ,
" groups # mentions_feed " ,
2020-05-12 23:08:35 +08:00
" list # user_topics_feed " ,
2020-05-12 20:35:36 +08:00
" list # category_feed " ,
2020-05-12 23:08:35 +08: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 20:35:36 +08:00
] ,
2020-10-07 00:20:15 +08: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-06 10:59:52 +08:00
) ,
2020-05-12 20:35:36 +08:00
]
2013-10-09 12:10:37 +08:00
# do all current user initialization here
2018-09-04 14:17:05 +08:00
def initialize ( env )
2013-10-09 12:10:37 +08: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-24 10:38:00 +08:00
# bypass if we have the shared session header
if shared_key = @env [ 'HTTP_X_SHARED_SESSION_KEY' ]
2019-12-03 17:05:53 +08:00
uid = Discourse . redis . get ( " shared_session_key_ #{ shared_key } " )
2014-10-24 10:38:00 +08:00
user = nil
if uid
user = User . find_by ( id : uid . to_i )
end
@env [ CURRENT_USER_KEY ] = user
return user
end
2014-05-23 06:13:25 +08:00
request = @request
2013-10-09 12:10:37 +08:00
2017-02-18 00:02:33 +08:00
user_api_key = @env [ USER_API_KEY ]
2020-05-12 20:35:36 +08: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-18 00:02:33 +08:00
auth_token = request . cookies [ TOKEN_COOKIE ] unless user_api_key || api_key
2013-10-09 12:10:37 +08:00
current_user = nil
if auth_token && auth_token . length == 32
2016-08-09 08:02:18 +08:00
limiter = RateLimiter . new ( nil , " cookie_auth_ #{ request . ip } " , COOKIE_ATTEMPTS_PER_MIN , 60 )
2018-02-10 08:09:54 +08:00
if limiter . can_perform?
2020-06-03 16:36:51 +08: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-14 03:01:01 +08:00
2017-02-01 06:21:37 +08:00
current_user = @user_token . try ( :user )
2016-08-09 08:02:18 +08:00
end
2016-07-28 10:58:49 +08:00
2018-03-06 13:49:31 +08:00
if ! current_user
@env [ BAD_TOKEN ] = true
2016-07-28 10:58:49 +08:00
begin
2018-09-04 14:17:05 +08:00
limiter . performed!
2016-07-28 10:58:49 +08:00
rescue RateLimiter :: LimitExceeded
2018-02-10 08:09:54 +08:00
raise Discourse :: InvalidAccess . new (
'Invalid Access' ,
nil ,
delete_cookie : TOKEN_COOKIE
)
2016-07-28 10:58:49 +08:00
end
end
2018-03-06 13:49:31 +08:00
elsif @env [ 'HTTP_DISCOURSE_LOGGED_IN' ]
@env [ BAD_TOKEN ] = true
2013-10-09 12:10:37 +08:00
end
# possible we have an api call, impersonate
2017-02-18 00:02:33 +08:00
if api_key
2014-05-23 06:13:25 +08:00
current_user = lookup_api_user ( api_key , request )
2017-10-20 22:30:13 +08:00
raise Discourse :: InvalidAccess . new ( I18n . t ( 'invalid_api_credentials' ) , nil , custom_message : " invalid_api_credentials " ) unless current_user
2017-02-18 00:02:33 +08:00
raise Discourse :: InvalidAccess if current_user . suspended? || ! current_user . active
2014-05-23 06:13:25 +08:00
@env [ API_KEY_ENV ] = true
2021-06-03 17:52:43 +08:00
rate_limit_admin_api_requests!
2013-10-09 12:10:37 +08:00
end
2016-08-15 15:58:33 +08:00
# user api key handling
2017-02-18 00:02:33 +08:00
if user_api_key
2016-08-15 15:58:33 +08:00
2021-10-22 00:43:26 +08: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 15:58:33 +08:00
unless limiter_day . can_perform?
2018-09-04 14:17:05 +08:00
limiter_day . performed!
2016-08-15 15:58:33 +08:00
end
unless limiter_min . can_perform?
2018-09-04 14:17:05 +08:00
limiter_min . performed!
2016-08-15 15:58:33 +08:00
end
2017-02-18 00:02:33 +08:00
current_user = lookup_user_api_user_and_update_key ( user_api_key , @env [ USER_API_CLIENT_ID ] )
2016-08-15 15:58:33 +08:00
raise Discourse :: InvalidAccess unless current_user
2017-02-18 00:02:33 +08:00
raise Discourse :: InvalidAccess if current_user . suspended? || ! current_user . active
2016-08-15 15:58:33 +08:00
2018-09-04 14:17:05 +08:00
limiter_min . performed!
limiter_day . performed!
2016-08-15 15:58:33 +08:00
2016-12-16 09:05:20 +08:00
@env [ USER_API_KEY_ENV ] = true
2016-08-15 15:58:33 +08:00
end
2017-02-18 00:02:33 +08: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 23:04:57 +08:00
if current_user && should_update_last_seen?
u = current_user
2020-03-11 14:42:56 +08:00
ip = request . ip
2018-07-18 23:04:57 +08:00
Scheduler :: Defer . later " Updating Last Seen " do
u . update_last_seen!
2020-03-11 14:42:56 +08:00
u . update_ip_address! ( ip )
2018-07-18 23:04:57 +08:00
end
end
2013-10-09 12:10:37 +08:00
@env [ CURRENT_USER_KEY ] = current_user
end
2016-07-25 10:07:31 +08:00
def refresh_session ( user , session , cookies )
2017-02-01 06:21:37 +08: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-18 00:02:33 +08:00
if ! is_user_api? && @user_token && @user_token . user == user
2017-02-01 06:21:37 +08: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-04 09:11:44 +08:00
if needs_rotation
2017-02-01 06:21:37 +08:00
if @user_token . rotate! ( user_agent : @env [ 'HTTP_USER_AGENT' ] ,
2017-03-08 02:27:34 +08:00
client_ip : @request . ip ,
path : @env [ 'REQUEST_PATH' ] )
2017-02-01 06:21:37 +08:00
cookies [ TOKEN_COOKIE ] = cookie_hash ( @user_token . unhashed_auth_token )
2020-04-15 00:32:24 +08:00
DiscourseEvent . trigger ( :user_session_refreshed , user )
2017-02-01 06:21:37 +08:00
end
end
2016-07-25 10:07:31 +08:00
end
2017-02-01 06:21:37 +08:00
2016-07-28 10:58:49 +08:00
if ! user && cookies . key? ( TOKEN_COOKIE )
2017-03-08 02:27:34 +08:00
cookies . delete ( TOKEN_COOKIE )
2016-07-28 10:58:49 +08:00
end
2016-07-25 10:07:31 +08:00
end
2018-11-12 22:34:12 +08:00
def log_on_user ( user , session , cookies , opts = { } )
2018-10-26 06:29:28 +08: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 22:34:12 +08:00
staff : user . staff? ,
2018-11-12 23:00:12 +08:00
impersonate : opts [ :impersonate ] )
2016-07-26 09:37:41 +08:00
2017-02-01 06:21:37 +08:00
cookies [ TOKEN_COOKIE ] = cookie_hash ( @user_token . unhashed_auth_token )
2020-03-17 23:48:24 +08:00
user . unstage!
2013-11-02 07:25:43 +08:00
make_developer_admin ( user )
2016-04-27 01:08:19 +08:00
enable_bootstrap_mode ( user )
2019-11-27 20:39:31 +08:00
UserAuthToken . enforce_session_count_limit! ( user . id )
2013-10-09 12:10:37 +08:00
@env [ CURRENT_USER_KEY ] = user
end
2017-02-01 06:21:37 +08:00
def cookie_hash ( unhashed_auth_token )
2017-02-24 01:01:28 +08:00
hash = {
2017-02-01 06:21:37 +08:00
value : unhashed_auth_token ,
2016-10-17 09:11:15 +08:00
httponly : true ,
2017-06-22 04:18:24 +08:00
secure : SiteSetting . force_https
2016-10-17 09:11:15 +08:00
}
2017-02-24 01:01:28 +08:00
2020-09-11 13:11:13 +08:00
if SiteSetting . persistent_sessions
hash [ :expires ] = SiteSetting . maximum_session_age . hours . from_now
end
2017-02-24 01:01:28 +08:00
if SiteSetting . same_site_cookies != " Disabled "
hash [ :same_site ] = SiteSetting . same_site_cookies
end
hash
2016-10-17 09:11:15 +08:00
end
2013-11-02 07:25:43 +08: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 15:03:39 +08:00
user . admin = true
user . save
2013-11-02 07:25:43 +08:00
end
end
2016-04-27 01:08:19 +08:00
def enable_bootstrap_mode ( user )
2018-05-13 23:00:02 +08: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-27 01:08:19 +08:00
end
2013-10-09 12:10:37 +08:00
def log_off_user ( session , cookies )
2017-02-01 06:21:37 +08:00
user = current_user
2018-05-13 23:00:02 +08:00
2017-02-01 06:21:37 +08:00
if SiteSetting . log_out_strict && user
user . user_auth_tokens . destroy_all
2016-05-18 15:27:54 +08:00
if user . admin && defined? ( Rack :: MiniProfiler )
# clear the profiling cookie to keep stuff tidy
2016-07-28 10:58:49 +08:00
cookies . delete ( " __profilin " )
2016-05-18 15:27:54 +08:00
end
2016-07-04 17:20:30 +08:00
user . logged_out
2017-02-01 06:21:37 +08:00
elsif user && @user_token
@user_token . destroy
2015-01-28 09:56:25 +08:00
end
2017-08-31 12:06:56 +08:00
2019-03-19 20:39:13 +08:00
cookies . delete ( 'authentication_data' )
2016-07-28 10:58:49 +08:00
cookies . delete ( TOKEN_COOKIE )
2013-10-09 12:10:37 +08:00
end
# api has special rights return true if api was detected
def is_api?
current_user
2016-12-16 09:05:20 +08:00
! ! ( @env [ API_KEY_ENV ] )
end
def is_user_api?
current_user
! ! ( @env [ USER_API_KEY_ENV ] )
2013-10-09 12:10:37 +08:00
end
def has_auth_cookie?
2014-05-23 06:13:25 +08:00
cookie = @request . cookies [ TOKEN_COOKIE ]
2013-10-09 12:10:37 +08:00
! cookie . nil? && cookie . length == 32
end
2014-05-23 06:13:25 +08:00
def should_update_last_seen?
2020-07-21 13:43:28 +08:00
return false unless can_write?
2019-01-22 18:07:48 +08:00
2019-04-16 00:34:34 +08:00
api = ! ! ( @env [ API_KEY_ENV ] ) || ! ! ( @env [ USER_API_KEY_ENV ] )
if @request . xhr? || api
2020-03-26 14:35:32 +08:00
@env [ " HTTP_DISCOURSE_PRESENT " ] == " true "
2017-03-01 01:34:57 +08:00
else
true
end
2014-05-23 06:13:25 +08:00
end
protected
2016-10-14 13:05:27 +08:00
def lookup_user_api_user_and_update_key ( user_api_key , client_id )
2020-09-29 17:57:48 +08:00
if api_key = UserApiKey . active . with_key ( user_api_key ) . includes ( :user , :scopes ) . first
2016-10-14 13:05:27 +08:00
unless api_key . allow? ( @env )
raise Discourse :: InvalidAccess
end
2020-07-21 13:43:28 +08:00
if can_write?
api_key . update_columns ( last_used_at : Time . zone . now )
2018-08-22 10:56:49 +08:00
2020-07-21 13:43:28 +08:00
if client_id . present? && client_id != api_key . client_id
2018-08-22 10:56:49 +08:00
2020-07-21 13:43:28 +08: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-22 10:56:49 +08:00
2020-07-21 13:43:28 +08:00
api_key . update_columns ( client_id : client_id )
end
2016-08-15 15:58:33 +08:00
end
api_key . user
end
end
2014-05-23 06:13:25 +08:00
def lookup_api_user ( api_key_value , request )
2019-12-12 19:45:00 +08:00
if api_key = ApiKey . active . with_key ( api_key_value ) . includes ( :user ) . first
2019-03-13 07:16:42 +08:00
api_username = header_api_key? ? @env [ HEADER_API_USERNAME ] : request [ API_USERNAME ]
2019-11-09 08:28:48 +08:00
2020-10-07 00:20:15 +08:00
unless api_key . request_allowed? ( @env )
2016-06-02 03:48:06 +08:00
Rails . logger . warn ( " [Unauthorized API Access] username: #{ api_username } , IP address: #{ request . ip } " )
2014-11-20 12:21:49 +08:00
return nil
end
2019-09-03 16:10:29 +08: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 13:43:28 +08:00
if user && can_write?
2019-09-03 16:10:29 +08:00
api_key . update_columns ( last_used_at : Time . zone . now )
2014-05-23 06:13:25 +08:00
end
2019-09-03 16:10:29 +08:00
user
2014-05-23 06:13:25 +08:00
end
end
2018-09-04 16:35:49 +08:00
private
2020-08-24 17:24:52 +08:00
def parameter_api_patterns
PARAMETER_API_PATTERNS + DiscoursePluginRegistry . api_parameter_routes
end
2020-05-12 20:35:36 +08: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-07 00:20:15 +08:00
parameter_api_patterns . any? { | p | p . match? ( env : @env ) }
2020-03-17 02:05:24 +08:00
end
2019-03-13 07:16:42 +08:00
def header_api_key?
! ! @env [ HEADER_API_KEY ]
end
2021-06-03 17:52:43 +08:00
def rate_limit_admin_api_requests!
2018-09-04 16:35:49 +08:00
return if Rails . env == " profile "
2021-06-03 17:52:43 +08: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 22:52:59 +08: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 17:52:43 +08:00
limit = [ GlobalSetting . max_admin_api_reqs_per_key_per_minute . to_i , limit ] . max
end
global_limit = RateLimiter . new (
2018-09-04 16:35:49 +08:00
nil ,
2021-06-03 17:52:43 +08:00
" admin_api_min " ,
limit ,
2018-09-04 16:35:49 +08:00
60
2021-06-03 17:52:43 +08:00
)
global_limit . performed!
2018-09-04 16:35:49 +08:00
end
2020-07-21 13:43:28 +08:00
def can_write?
@can_write || = ! Discourse . pg_readonly_mode?
end
2013-10-09 12:10:37 +08:00
end