Merge branch '17829-remove-omniauth' refs #17829
[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   # create a new session
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       return send_error "Legacy code path no longer supported", status: 404
31     end
32
33     if !authinfo['user_uuid'].blank?
34       user = User.find_by_uuid(authinfo['user_uuid'])
35       if !user
36         Rails.logger.warn "Nonexistent user_uuid in authinfo #{authinfo.inspect}"
37         return redirect_to login_failure_url
38       end
39     else
40       begin
41         user = User.register(authinfo)
42       rescue => e
43         Rails.logger.warn "User.register error #{e}"
44         Rails.logger.warn "authinfo was #{authinfo.inspect}"
45         return redirect_to login_failure_url
46       end
47     end
48
49     # For the benefit of functional and integration tests:
50     @user = user
51
52     if user.uuid[0..4] != Rails.configuration.ClusterID
53       # Actually a remote user
54       # Send them to their home cluster's login
55       rh = Rails.configuration.RemoteClusters[user.uuid[0..4]]
56       remote, return_to_url = params[:return_to].split(',', 2)
57       @remotehomeurl = "#{rh.Scheme || "https"}://#{rh.Host}/login?remote=#{Rails.configuration.ClusterID}&return_to=#{return_to_url}"
58       render
59       return
60     end
61
62     # prevent ArvadosModel#before_create and _update from throwing
63     # "unauthorized":
64     Thread.current[:user] = user
65
66     user.save or raise Exception.new(user.errors.messages)
67
68     # Give the authenticated user a cookie for direct API access
69     session[:user_id] = user.id
70     session[:api_client_uuid] = nil
71     session[:api_client_trusted] = true # full permission to see user's secrets
72
73     @redirect_to = root_path
74     if params.has_key?(:return_to)
75       # return_to param's format is 'remote,return_to_url'. This comes from login()
76       # encoding the remote=zbbbb parameter passed by a client asking for a salted
77       # token.
78       remote, return_to_url = params[:return_to].split(',', 2)
79       if remote !~ /^[0-9a-z]{5}$/ && remote != ""
80         return send_error 'Invalid remote cluster id', status: 400
81       end
82       remote = nil if remote == ''
83       return send_api_token_to(return_to_url, user, remote, max_expires_at)
84     end
85     redirect_to @redirect_to
86   end
87
88   # Omniauth failure callback
89   def failure
90     flash[:notice] = params[:message]
91   end
92
93   # logout - this gets intercepted by controller, so this is probably
94   # mostly dead code at this point.
95   def logout
96     session[:user_id] = nil
97
98     flash[:notice] = 'You have logged off'
99     return_to = params[:return_to] || root_url
100     redirect_to return_to
101   end
102
103   # login.  Redirect to LoginCluster.
104   def login
105     if params[:remote] !~ /^[0-9a-z]{5}$/ && !params[:remote].nil?
106       return send_error 'Invalid remote cluster id', status: 400
107     end
108     if current_user and params[:return_to]
109       # Already logged in; just need to send a token to the requesting
110       # API client.
111       #
112       # FIXME: if current_user has never authorized this app before,
113       # ask for confirmation here!
114
115       return send_api_token_to(params[:return_to], current_user, params[:remote])
116     end
117     p = []
118     p << "auth_provider=#{CGI.escape(params[:auth_provider])}" if params[:auth_provider]
119
120     if !Rails.configuration.Login.LoginCluster.empty? and Rails.configuration.Login.LoginCluster != Rails.configuration.ClusterID
121       host = ApiClientAuthorization.remote_host(uuid_prefix: Rails.configuration.Login.LoginCluster)
122       if not host
123         raise "LoginCluster #{Rails.configuration.Login.LoginCluster} missing from RemoteClusters"
124       end
125       scheme = "https"
126       cluster = Rails.configuration.RemoteClusters[Rails.configuration.Login.LoginCluster]
127       if cluster and cluster['Scheme'] and !cluster['Scheme'].empty?
128         scheme = cluster['Scheme']
129       end
130       login_cluster = "#{scheme}://#{host}"
131       p << "remote=#{CGI.escape(params[:remote])}" if params[:remote]
132       p << "return_to=#{CGI.escape(params[:return_to])}" if params[:return_to]
133       redirect_to "#{login_cluster}/login?#{p.join('&')}"
134     else
135       return send_error "Legacy code path no longer supported", status: 404
136     end
137   end
138
139   def send_api_token_to(callback_url, user, remote=nil, token_expiration=nil)
140     # Give the API client a token for making API calls on behalf of
141     # the authenticated user
142
143     # Stub: automatically register all new API clients
144     api_client_url_prefix = callback_url.match(%r{^.*?://[^/]+})[0] + '/'
145     act_as_system_user do
146       @api_client = ApiClient.
147         find_or_create_by(url_prefix: api_client_url_prefix)
148     end
149     if Rails.configuration.Login.TokenLifetime > 0
150       if token_expiration == nil
151         token_expiration = db_current_time + Rails.configuration.Login.TokenLifetime
152       else
153         token_expiration = [token_expiration, db_current_time + Rails.configuration.Login.TokenLifetime].min
154       end
155     end
156
157     @api_client_auth = ApiClientAuthorization.
158       new(user: user,
159           api_client: @api_client,
160           created_by_ip_address: remote_ip,
161           expires_at: token_expiration,
162           scopes: ["all"])
163     @api_client_auth.save!
164
165     if callback_url.index('?')
166       callback_url += '&'
167     else
168       callback_url += '?'
169     end
170     if remote.nil?
171       token = @api_client_auth.token
172     else
173       token = @api_client_auth.salted_token(remote: remote)
174     end
175     callback_url += 'api_token=' + token
176     redirect_to callback_url
177   end
178
179   def cross_origin_forbidden
180     send_error 'Forbidden', status: 403
181   end
182 end