/
api_controller.rb
195 lines (176 loc) · 6.68 KB
/
api_controller.rb
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
class ApiController < ApplicationController
skip_before_action :verify_authenticity_token
private
##
# Set allowed request formats if no explicit format has been
# requested via a URL suffix. Allowed formats are taken from
# any HTTP Accept header with XML as the default.
def set_request_formats
unless params[:format]
accept_header = request.headers["HTTP_ACCEPT"]
if accept_header
# Some clients (such asJOSM) send Accept headers which cannot be
# parse by Rails, for example:
#
# Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
#
# where both "*" and ".2" as a quality do not adhere to the syntax
# described in RFC 7231, section 5.3.1, etc.
#
# As a workaround, and for back compatibility, default to XML format.
mimetypes = begin
Mime::Type.parse(accept_header)
rescue Mime::Type::InvalidMimeType
Array(Mime[:xml])
end
# Allow XML and JSON formats, and treat an all formats wildcard
# as XML for backwards compatibility - all other formats are discarded
# which will result in a 406 Not Acceptable response being sent
formats = mimetypes.map do |mime|
if mime.symbol == :xml || mime == "*/*" then :xml
elsif mime.symbol == :json then :json
end
end
else
# Default to XML if no accept header was sent - this includes
# the unit tests which don't set one by default
formats = Array(:xml)
end
request.formats = formats.compact
end
end
def authorize(realm = "Web Password", errormessage = "Couldn't authenticate you")
# make the current_user object from any auth sources we have
setup_user_auth
# handle authenticate pass/fail
unless current_user
# no auth, the user does not exist or the password was wrong
if Settings.basic_auth_support
response.headers["WWW-Authenticate"] = "Basic realm=\"#{realm}\""
render :plain => errormessage, :status => :unauthorized
else
render :plain => errormessage, :status => :forbidden
end
false
end
end
def current_ability
# Use capabilities from the oauth token if it exists and is a valid access token
if doorkeeper_token&.accessible?
ApiAbility.new(nil).merge(ApiCapability.new(doorkeeper_token))
elsif Authenticator.new(self, [:token]).allow?
ApiAbility.new(nil).merge(ApiCapability.new(current_token))
else
ApiAbility.new(current_user)
end
end
def deny_access(_exception)
if doorkeeper_token || current_token
set_locale
report_error t("oauth.permissions.missing"), :forbidden
elsif current_user
head :forbidden
elsif Settings.basic_auth_support
realm = "Web Password"
errormessage = "Couldn't authenticate you"
response.headers["WWW-Authenticate"] = "Basic realm=\"#{realm}\""
render :plain => errormessage, :status => :unauthorized
else
render :plain => errormessage, :status => :forbidden
end
end
def gpx_status
status = database_status
status = "offline" if status == "online" && Settings.status == "gpx_offline"
status
end
##
# sets up the current_user for use by other methods. this is mostly called
# from the authorize method, but can be called elsewhere if authorisation
# is optional.
def setup_user_auth
logger.info " setup_user_auth"
# try and setup using OAuth
if doorkeeper_token&.accessible?
self.current_user = User.find(doorkeeper_token.resource_owner_id)
elsif Authenticator.new(self, [:token]).allow?
# self.current_user setup by OAuth
elsif Settings.basic_auth_support
username, passwd = auth_data # parse from headers
# authenticate per-scheme
self.current_user = if username.nil?
nil # no authentication provided - perhaps first connect (client should retry after 401)
else
User.authenticate(:username => username, :password => passwd) # basic auth
end
# log if we have authenticated using basic auth
logger.info "Authenticated as user #{current_user.id} using basic authentication" if current_user
end
# have we identified the user?
if current_user
# check if the user has been banned
user_block = current_user.blocks.active.take
unless user_block.nil?
set_locale
if user_block.zero_hour?
report_error t("application.setup_user_auth.blocked_zero_hour"), :forbidden
else
report_error t("application.setup_user_auth.blocked"), :forbidden
end
end
# if the user hasn't seen the contributor terms then don't
# allow editing - they have to go to the web site and see
# (but can decline) the CTs to continue.
if !current_user.terms_seen && flash[:skip_terms].nil?
set_locale
report_error t("application.setup_user_auth.need_to_see_terms"), :forbidden
end
end
end
def api_call_handle_error
yield
rescue ActionController::UnknownFormat
head :not_acceptable
rescue ActiveRecord::RecordNotFound => e
head :not_found
rescue LibXML::XML::Error, ArgumentError => e
report_error e.message, :bad_request
rescue ActiveRecord::RecordInvalid => e
message = "#{e.record.class} #{e.record.id}: "
e.record.errors.each { |error| message << "#{error.attribute}: #{error.message} (#{e.record[error.attribute].inspect})" }
report_error message, :bad_request
rescue OSM::APIError => e
report_error e.message, e.status
rescue AbstractController::ActionNotFound => e
raise
rescue StandardError => e
logger.info("API threw unexpected #{e.class} exception: #{e.message}")
e.backtrace.each { |l| logger.info(l) }
report_error "#{e.class}: #{e.message}", :internal_server_error
end
##
# wrap an api call in a timeout
def api_call_timeout(&block)
Timeout.timeout(Settings.api_timeout, &block)
rescue ActionView::Template::Error => e
e = e.cause
if e.is_a?(Timeout::Error) ||
(e.is_a?(ActiveRecord::StatementInvalid) && e.message.include?("execution expired"))
ActiveRecord::Base.connection.raw_connection.cancel
raise OSM::APITimeoutError
else
raise
end
rescue Timeout::Error
ActiveRecord::Base.connection.raw_connection.cancel
raise OSM::APITimeoutError
end
##
# check the api change rate limit
def check_rate_limit(new_changes = 1)
max_changes = ActiveRecord::Base.connection.select_value(
"SELECT api_rate_limit($1)", "api_rate_limit", [current_user.id]
)
raise OSM::APIRateLimitExceeded if new_changes > max_changes
end
end