18004: Fixes a couple of race condition bugs related to caching remote users.
[arvados.git] / services / api / app / controllers / user_sessions_controller.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 class UserSessionsController < ApplicationController
6   before_action :require_auth_scope, :only => [ :destroy ]
7
8   skip_before_action :set_cors_headers
9   skip_before_action :find_object_by_uuid
10   skip_before_action :render_404_if_no_object
11
12   respond_to :html
13
14   # omniauth callback method
15   def create
16     if !Rails.configuration.Login.LoginCluster.empty? and Rails.configuration.Login.LoginCluster != Rails.configuration.ClusterID
17       raise "Local login disabled when LoginCluster is set"
18     end
19
20     max_expires_at = nil
21     if params[:provider] == 'controller'
22       if request.headers['Authorization'] != 'Bearer ' + Rails.configuration.SystemRootToken
23         return send_error('Invalid authorization header', status: 401)
24       end
25       # arvados-controller verified the user and is passing auth_info
26       # in request params.
27       authinfo = SafeJSON.load(params[:auth_info])
28       max_expires_at = authinfo["expires_at"]
29     else
30       # omniauth middleware verified the user and is passing auth_info
31       # in request.env.
32       authinfo = request.env['omniauth.auth']['info'].with_indifferent_access
33     end
34
35     if !authinfo['user_uuid'].blank?
36       user = User.find_by_uuid(authinfo['user_uuid'])
37       if !user
38         Rails.logger.warn "Nonexistent user_uuid in authinfo #{authinfo.inspect}"
39         return redirect_to login_failure_url
40       end
41     else
42       begin
43         user = User.register(authinfo)
44       rescue => e
45         Rails.logger.warn "User.register error #{e}"
46         Rails.logger.warn "authinfo was #{authinfo.inspect}"
47         return redirect_to login_failure_url
48       end
49     end
50
51     # For the benefit of functional and integration tests:
52     @user = user
53
54     if user.uuid[0..4] != Rails.configuration.ClusterID
55       # Actually a remote user
56       # Send them to their home cluster's login
57       rh = Rails.configuration.RemoteClusters[user.uuid[0..4]]
58       remote, return_to_url = params[:return_to].split(',', 2)
59       @remotehomeurl = "#{rh.Scheme || "https"}://#{rh.Host}/login?remote=#{Rails.configuration.ClusterID}&return_to=#{return_to_url}"
60       render
61       return
62     end
63
64     # prevent ArvadosModel#before_create and _update from throwing
65     # "unauthorized":
66     Thread.current[:user] = user
67
68     user.save or raise Exception.new(user.errors.messages)
69
70     # Give the authenticated user a cookie for direct API access
71     session[:user_id] = user.id
72     session[:api_client_uuid] = nil
73     session[:api_client_trusted] = true # full permission to see user's secrets
74
75     @redirect_to = root_path
76     if params.has_key?(:return_to)
77       # return_to param's format is 'remote,return_to_url'. This comes from login()
78       # encoding the remote=zbbbb parameter passed by a client asking for a salted
79       # token.
80       remote, return_to_url = params[:return_to].split(',', 2)
81       if remote !~ /^[0-9a-z]{5}$/ && remote != ""
82         return send_error 'Invalid remote cluster id', status: 400
83       end
84       remote = nil if remote == ''
85       return send_api_token_to(return_to_url, user, remote, max_expires_at)
86     end
87     redirect_to @redirect_to
88   end
89
90   # Omniauth failure callback
91   def failure
92     flash[:notice] = params[:message]
93   end
94
95   # logout - Clear our rack session BUT essentially redirect to the provider
96   # to clean up the Devise session from there too !
97   def logout
98     session[:user_id] = nil
99
100     flash[:notice] = 'You have logged off'
101     return_to = params[:return_to] || root_url
102     redirect_to "#{Rails.configuration.Services.SSO.ExternalURL}users/sign_out?redirect_uri=#{CGI.escape return_to}"
103   end
104
105   # login - Just bounce to /auth/joshid. The only purpose of this function is
106   # to save the return_to parameter (if it exists; see the application
107   # controller). /auth/joshid bypasses the application controller.
108   def login
109     if params[:remote] !~ /^[0-9a-z]{5}$/ && !params[:remote].nil?
110       return send_error 'Invalid remote cluster id', status: 400
111     end
112     if current_user and params[:return_to]
113       # Already logged in; just need to send a token to the requesting
114       # API client.
115       #
116       # FIXME: if current_user has never authorized this app before,
117       # ask for confirmation here!
118
119       return send_api_token_to(params[:return_to], current_user, params[:remote])
120     end
121     p = []
122     p << "auth_provider=#{CGI.escape(params[:auth_provider])}" if params[:auth_provider]
123
124     if !Rails.configuration.Login.LoginCluster.empty? and Rails.configuration.Login.LoginCluster != Rails.configuration.ClusterID
125       host = ApiClientAuthorization.remote_host(uuid_prefix: Rails.configuration.Login.LoginCluster)
126       if not host
127         raise "LoginCluster #{Rails.configuration.Login.LoginCluster} missing from RemoteClusters"
128       end
129       scheme = "https"
130       cluster = Rails.configuration.RemoteClusters[Rails.configuration.Login.LoginCluster]
131       if cluster and cluster['Scheme'] and !cluster['Scheme'].empty?
132         scheme = cluster['Scheme']
133       end
134       login_cluster = "#{scheme}://#{host}"
135       p << "remote=#{CGI.escape(params[:remote])}" if params[:remote]
136       p << "return_to=#{CGI.escape(params[:return_to])}" if params[:return_to]
137       redirect_to "#{login_cluster}/login?#{p.join('&')}"
138     else
139       if params[:return_to]
140         # Encode remote param inside callback's return_to, so that we'll get it on
141         # create() after login.
142         remote_param = params[:remote].nil? ? '' : params[:remote]
143         p << "return_to=#{CGI.escape(remote_param + ',' + params[:return_to])}"
144       end
145       redirect_to "/auth/joshid?#{p.join('&')}"
146     end
147   end
148
149   def send_api_token_to(callback_url, user, remote=nil, token_expiration=nil)
150     # Give the API client a token for making API calls on behalf of
151     # the authenticated user
152
153     # Stub: automatically register all new API clients
154     api_client_url_prefix = callback_url.match(%r{^.*?://[^/]+})[0] + '/'
155     act_as_system_user do
156       @api_client = ApiClient.
157         find_or_create_by(url_prefix: api_client_url_prefix)
158     end
159     if Rails.configuration.Login.TokenLifetime > 0
160       if token_expiration == nil
161         token_expiration = db_current_time + Rails.configuration.Login.TokenLifetime
162       else
163         token_expiration = [token_expiration, db_current_time + Rails.configuration.Login.TokenLifetime].min
164       end
165     end
166
167     @api_client_auth = ApiClientAuthorization.
168       new(user: user,
169           api_client: @api_client,
170           created_by_ip_address: remote_ip,
171           expires_at: token_expiration,
172           scopes: ["all"])
173     @api_client_auth.save!
174
175     if callback_url.index('?')
176       callback_url += '&'
177     else
178       callback_url += '?'
179     end
180     if remote.nil?
181       token = @api_client_auth.token
182     else
183       token = @api_client_auth.salted_token(remote: remote)
184     end
185     callback_url += 'api_token=' + token
186     redirect_to callback_url
187   end
188
189   def cross_origin_forbidden
190     send_error 'Forbidden', status: 403
191   end
192 end