Move the authorize method to api_controller
[rails.git] / app / controllers / application_controller.rb
1 class ApplicationController < ActionController::Base
2   include SessionPersistence
3
4   protect_from_forgery :with => :exception
5
6   rescue_from CanCan::AccessDenied, :with => :deny_access
7   check_authorization
8
9   before_action :fetch_body
10   around_action :better_errors_allow_inline, :if => proc { Rails.env.development? }
11
12   attr_accessor :current_user
13   helper_method :current_user
14
15   def authorize_web
16     if session[:user]
17       self.current_user = User.where(:id => session[:user]).where("status IN ('active', 'confirmed', 'suspended')").first
18
19       if current_user.status == "suspended"
20         session.delete(:user)
21         session_expires_automatically
22
23         redirect_to :controller => "users", :action => "suspended"
24
25       # don't allow access to any auth-requiring part of the site unless
26       # the new CTs have been seen (and accept/decline chosen).
27       elsif !current_user.terms_seen && flash[:skip_terms].nil?
28         flash[:notice] = t "users.terms.you need to accept or decline"
29         if params[:referer]
30           redirect_to :controller => "users", :action => "terms", :referer => params[:referer]
31         else
32           redirect_to :controller => "users", :action => "terms", :referer => request.fullpath
33         end
34       end
35     elsif session[:token]
36       session[:user] = current_user.id if self.current_user = User.authenticate(:token => session[:token])
37     end
38   rescue StandardError => ex
39     logger.info("Exception authorizing user: #{ex}")
40     reset_session
41     self.current_user = nil
42   end
43
44   def require_user
45     unless current_user
46       if request.get?
47         redirect_to :controller => "users", :action => "login", :referer => request.fullpath
48       else
49         head :forbidden
50       end
51     end
52   end
53
54   def require_oauth
55     @oauth = current_user.access_token(Settings.oauth_key) if current_user && Settings.key?(:oauth_key)
56   end
57
58   ##
59   # require the user to have cookies enabled in their browser
60   def require_cookies
61     if request.cookies["_osm_session"].to_s == ""
62       if params[:cookie_test].nil?
63         session[:cookie_test] = true
64         redirect_to params.to_unsafe_h.merge(:cookie_test => "true")
65         false
66       else
67         flash.now[:warning] = t "application.require_cookies.cookies_needed"
68       end
69     else
70       session.delete(:cookie_test)
71     end
72   end
73
74   ##
75   # sets up the current_user for use by other methods. this is mostly called
76   # from the authorize method, but can be called elsewhere if authorisation
77   # is optional.
78   def setup_user_auth
79     # try and setup using OAuth
80     unless Authenticator.new(self, [:token]).allow?
81       username, passwd = get_auth_data # parse from headers
82       # authenticate per-scheme
83       self.current_user = if username.nil?
84                             nil # no authentication provided - perhaps first connect (client should retry after 401)
85                           elsif username == "token"
86                             User.authenticate(:token => passwd) # preferred - random token for user from db, passed in basic auth
87                           else
88                             User.authenticate(:username => username, :password => passwd) # basic auth
89                           end
90     end
91
92     # have we identified the user?
93     if current_user
94       # check if the user has been banned
95       user_block = current_user.blocks.active.take
96       unless user_block.nil?
97         set_locale
98         if user_block.zero_hour?
99           report_error t("application.setup_user_auth.blocked_zero_hour"), :forbidden
100         else
101           report_error t("application.setup_user_auth.blocked"), :forbidden
102         end
103       end
104
105       # if the user hasn't seen the contributor terms then don't
106       # allow editing - they have to go to the web site and see
107       # (but can decline) the CTs to continue.
108       if !current_user.terms_seen && flash[:skip_terms].nil?
109         set_locale
110         report_error t("application.setup_user_auth.need_to_see_terms"), :forbidden
111       end
112     end
113   end
114
115   def check_database_readable(need_api = false)
116     if Settings.status == "database_offline" || (need_api && Settings.status == "api_offline")
117       if request.xhr?
118         report_error "Database offline for maintenance", :service_unavailable
119       else
120         redirect_to :controller => "site", :action => "offline"
121       end
122     end
123   end
124
125   def check_database_writable(need_api = false)
126     if Settings.status == "database_offline" || Settings.status == "database_readonly" ||
127        (need_api && (Settings.status == "api_offline" || Settings.status == "api_readonly"))
128       if request.xhr?
129         report_error "Database offline for maintenance", :service_unavailable
130       else
131         redirect_to :controller => "site", :action => "offline"
132       end
133     end
134   end
135
136   def check_api_readable
137     if api_status == :offline
138       report_error "Database offline for maintenance", :service_unavailable
139       false
140     end
141   end
142
143   def check_api_writable
144     unless api_status == :online
145       report_error "Database offline for maintenance", :service_unavailable
146       false
147     end
148   end
149
150   def database_status
151     if Settings.status == "database_offline"
152       :offline
153     elsif Settings.status == "database_readonly"
154       :readonly
155     else
156       :online
157     end
158   end
159
160   def api_status
161     status = database_status
162     if status == :online
163       if Settings.status == "api_offline"
164         status = :offline
165       elsif Settings.status == "api_readonly"
166         status = :readonly
167       end
168     end
169     status
170   end
171
172   def gpx_status
173     status = database_status
174     status = :offline if status == :online && Settings.status == "gpx_offline"
175     status
176   end
177
178   def require_public_data
179     unless current_user.data_public?
180       report_error "You must make your edits public to upload new data", :forbidden
181       false
182     end
183   end
184
185   # Report and error to the user
186   # (If anyone ever fixes Rails so it can set a http status "reason phrase",
187   #  rather than only a status code and having the web engine make up a
188   #  phrase from that, we can also put the error message into the status
189   #  message. For now, rails won't let us)
190   def report_error(message, status = :bad_request)
191     # TODO: some sort of escaping of problem characters in the message
192     response.headers["Error"] = message
193
194     if request.headers["X-Error-Format"]&.casecmp("xml")&.zero?
195       result = OSM::API.new.get_xml_doc
196       result.root.name = "osmError"
197       result.root << (XML::Node.new("status") << "#{Rack::Utils.status_code(status)} #{Rack::Utils::HTTP_STATUS_CODES[status]}")
198       result.root << (XML::Node.new("message") << message)
199
200       render :xml => result.to_s
201     else
202       render :plain => message, :status => status
203     end
204   end
205
206   def preferred_languages(reset = false)
207     @preferred_languages = nil if reset
208     @preferred_languages ||= if params[:locale]
209                                Locale.list(params[:locale])
210                              elsif current_user
211                                current_user.preferred_languages
212                              else
213                                Locale.list(http_accept_language.user_preferred_languages)
214                              end
215   end
216
217   helper_method :preferred_languages
218
219   def set_locale(reset = false)
220     if current_user&.languages&.empty? && !http_accept_language.user_preferred_languages.empty?
221       current_user.languages = http_accept_language.user_preferred_languages
222       current_user.save
223     end
224
225     I18n.locale = Locale.available.preferred(preferred_languages(reset))
226
227     response.headers["Vary"] = "Accept-Language"
228     response.headers["Content-Language"] = I18n.locale.to_s
229   end
230
231   def api_call_handle_error
232     yield
233   rescue ActiveRecord::RecordNotFound => ex
234     head :not_found
235   rescue LibXML::XML::Error, ArgumentError => ex
236     report_error ex.message, :bad_request
237   rescue ActiveRecord::RecordInvalid => ex
238     message = "#{ex.record.class} #{ex.record.id}: "
239     ex.record.errors.each { |attr, msg| message << "#{attr}: #{msg} (#{ex.record[attr].inspect})" }
240     report_error message, :bad_request
241   rescue OSM::APIError => ex
242     report_error ex.message, ex.status
243   rescue AbstractController::ActionNotFound => ex
244     raise
245   rescue StandardError => ex
246     logger.info("API threw unexpected #{ex.class} exception: #{ex.message}")
247     ex.backtrace.each { |l| logger.info(l) }
248     report_error "#{ex.class}: #{ex.message}", :internal_server_error
249   end
250
251   ##
252   # asserts that the request method is the +method+ given as a parameter
253   # or raises a suitable error. +method+ should be a symbol, e.g: :put or :get.
254   def assert_method(method)
255     ok = request.send((method.to_s.downcase + "?").to_sym)
256     raise OSM::APIBadMethodError, method unless ok
257   end
258
259   ##
260   # wrap an api call in a timeout
261   def api_call_timeout
262     OSM::Timer.timeout(Settings.api_timeout, Timeout::Error) do
263       yield
264     end
265   rescue Timeout::Error
266     raise OSM::APITimeoutError
267   end
268
269   ##
270   # wrap a web page in a timeout
271   def web_timeout
272     OSM::Timer.timeout(Settings.web_timeout, Timeout::Error) do
273       yield
274     end
275   rescue ActionView::Template::Error => ex
276     ex = ex.cause
277
278     if ex.is_a?(Timeout::Error) ||
279        (ex.is_a?(ActiveRecord::StatementInvalid) && ex.message =~ /execution expired/)
280       render :action => "timeout"
281     else
282       raise
283     end
284   rescue Timeout::Error
285     render :action => "timeout"
286   end
287
288   ##
289   # ensure that there is a "user" instance variable
290   def lookup_user
291     render_unknown_user params[:display_name] unless @user = User.active.find_by(:display_name => params[:display_name])
292   end
293
294   ##
295   # render a "no such user" page
296   def render_unknown_user(name)
297     @title = t "users.no_such_user.title"
298     @not_found_user = name
299
300     respond_to do |format|
301       format.html { render :template => "users/no_such_user", :status => :not_found }
302       format.all { head :not_found }
303     end
304   end
305
306   ##
307   # Unfortunately if a PUT or POST request that has a body fails to
308   # read it then Apache will sometimes fail to return the response it
309   # is given to the client properly, instead erroring:
310   #
311   #   https://issues.apache.org/bugzilla/show_bug.cgi?id=44782
312   #
313   # To work round this we call rewind on the body here, which is added
314   # as a filter, to force it to be fetched from Apache into a file.
315   def fetch_body
316     request.body.rewind
317   end
318
319   def map_layout
320     append_content_security_policy_directives(
321       :child_src => %w[http://127.0.0.1:8111 https://127.0.0.1:8112],
322       :frame_src => %w[http://127.0.0.1:8111 https://127.0.0.1:8112],
323       :connect_src => [Settings.nominatim_url, Settings.overpass_url, Settings.fossgis_osrm_url, Settings.graphhopper_url],
324       :form_action => %w[render.openstreetmap.org],
325       :style_src => %w['unsafe-inline']
326     )
327
328     if Settings.status == "database_offline" || Settings.status == "api_offline"
329       flash.now[:warning] = t("layouts.osm_offline")
330     elsif Settings.status == "database_readonly" || Settings.status == "api_readonly"
331       flash.now[:warning] = t("layouts.osm_read_only")
332     end
333
334     request.xhr? ? "xhr" : "map"
335   end
336
337   def allow_thirdparty_images
338     append_content_security_policy_directives(:img_src => %w[*])
339   end
340
341   def preferred_editor
342     editor = if params[:editor]
343                params[:editor]
344              elsif current_user&.preferred_editor
345                current_user.preferred_editor
346              else
347                Settings.default_editor
348              end
349
350     editor
351   end
352
353   helper_method :preferred_editor
354
355   def update_totp
356     if Settings.key?(:totp_key)
357       cookies["_osm_totp_token"] = {
358         :value => ROTP::TOTP.new(Settings.totp_key, :interval => 3600).now,
359         :domain => "openstreetmap.org",
360         :expires => 1.hour.from_now
361       }
362     end
363   end
364
365   def better_errors_allow_inline
366     yield
367   rescue StandardError
368     append_content_security_policy_directives(
369       :script_src => %w['unsafe-inline'],
370       :style_src => %w['unsafe-inline']
371     )
372
373     raise
374   end
375
376   def current_ability
377     # Use capabilities from the oauth token if it exists and is a valid access token
378     if Authenticator.new(self, [:token]).allow?
379       Ability.new(nil).merge(Capability.new(current_token))
380     else
381       Ability.new(current_user)
382     end
383   end
384
385   def deny_access(_exception)
386     if current_token
387       set_locale
388       report_error t("oauth.permissions.missing"), :forbidden
389     elsif current_user
390       set_locale
391       respond_to do |format|
392         format.html { redirect_to :controller => "errors", :action => "forbidden" }
393         format.any { report_error t("application.permission_denied"), :forbidden }
394       end
395     elsif request.get?
396       respond_to do |format|
397         format.html { redirect_to :controller => "users", :action => "login", :referer => request.fullpath }
398         format.any { head :forbidden }
399       end
400     else
401       head :forbidden
402     end
403   end
404
405   private
406
407   # extract authorisation credentials from headers, returns user = nil if none
408   def get_auth_data
409     if request.env.key? "X-HTTP_AUTHORIZATION" # where mod_rewrite might have put it
410       authdata = request.env["X-HTTP_AUTHORIZATION"].to_s.split
411     elsif request.env.key? "REDIRECT_X_HTTP_AUTHORIZATION" # mod_fcgi
412       authdata = request.env["REDIRECT_X_HTTP_AUTHORIZATION"].to_s.split
413     elsif request.env.key? "HTTP_AUTHORIZATION" # regular location
414       authdata = request.env["HTTP_AUTHORIZATION"].to_s.split
415     end
416     # only basic authentication supported
417     user, pass = Base64.decode64(authdata[1]).split(":", 2) if authdata && authdata[0] == "Basic"
418     [user, pass]
419   end
420
421   # override to stop oauth plugin sending errors
422   def invalid_oauth_response; end
423 end