15529: Add some checks to login method
[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     omniauth = request.env['omniauth.auth']
17
18     identity_url_ok = (omniauth['info']['identity_url'].length > 0) rescue false
19     unless identity_url_ok
20       # Whoa. This should never happen.
21       logger.error "UserSessionsController.create: omniauth object missing/invalid"
22       logger.error "omniauth: "+omniauth.pretty_inspect
23
24       return redirect_to login_failure_url
25     end
26
27     # Only local users can create sessions, hence uuid_like_pattern
28     # here.
29     user = User.unscoped.where('identity_url = ? and uuid like ?',
30                                omniauth['info']['identity_url'],
31                                User.uuid_like_pattern).first
32     if not user
33       # Check for permission to log in to an existing User record with
34       # a different identity_url
35       Link.where("link_class = ? and name = ? and tail_uuid = ? and head_uuid like ?",
36                  'permission',
37                  'can_login',
38                  omniauth['info']['email'],
39                  User.uuid_like_pattern).each do |link|
40         if prefix = link.properties['identity_url_prefix']
41           if prefix == omniauth['info']['identity_url'][0..prefix.size-1]
42             user = User.find_by_uuid(link.head_uuid)
43             break if user
44           end
45         end
46       end
47     end
48
49     if not user
50       # New user registration
51       user = User.new(:email => omniauth['info']['email'],
52                       :first_name => omniauth['info']['first_name'],
53                       :last_name => omniauth['info']['last_name'],
54                       :identity_url => omniauth['info']['identity_url'],
55                       :is_active => Rails.configuration.Users.NewUsersAreActive,
56                       :owner_uuid => system_user_uuid)
57       if omniauth['info']['username']
58         user.set_initial_username(requested: omniauth['info']['username'])
59       end
60       act_as_system_user do
61         user.save or raise Exception.new(user.errors.messages)
62       end
63     else
64       user.email = omniauth['info']['email']
65       user.first_name = omniauth['info']['first_name']
66       user.last_name = omniauth['info']['last_name']
67       if user.identity_url.nil?
68         # First login to a pre-activated account
69         user.identity_url = omniauth['info']['identity_url']
70       end
71
72       while (uuid = user.redirect_to_user_uuid)
73         user = User.unscoped.where(uuid: uuid).first
74         if !user
75           raise Exception.new("identity_url #{omniauth['info']['identity_url']} redirects to nonexistent uuid #{uuid}")
76         end
77       end
78     end
79
80     # For the benefit of functional and integration tests:
81     @user = user
82
83     if user.uuid[0..4] != Rails.configuration.ClusterID
84       # Actually a remote user
85       # Send them to their home cluster's login
86       rh = Rails.configuration.RemoteClusters[user.uuid[0..4]]
87       remote, return_to_url = params[:return_to].split(',', 2)
88       @remotehomeurl = "#{rh.Scheme || "https"}://#{rh.Host}/login?remote=#{Rails.configuration.ClusterID}&return_to=#{return_to_url}"
89       render
90       return
91     end
92
93     # prevent ArvadosModel#before_create and _update from throwing
94     # "unauthorized":
95     Thread.current[:user] = user
96
97     user.save or raise Exception.new(user.errors.messages)
98
99     omniauth.delete('extra')
100
101     # Give the authenticated user a cookie for direct API access
102     session[:user_id] = user.id
103     session[:api_client_uuid] = nil
104     session[:api_client_trusted] = true # full permission to see user's secrets
105
106     @redirect_to = root_path
107     if params.has_key?(:return_to)
108       # return_to param's format is 'remote,return_to_url'. This comes from login()
109       # encoding the remote=zbbbb parameter passed by a client asking for a salted
110       # token.
111       remote, return_to_url = params[:return_to].split(',', 2)
112       if remote !~ /^[0-9a-z]{5}$/ && remote != ""
113         return send_error 'Invalid remote cluster id', status: 400
114       end
115       remote = nil if remote == ''
116       return send_api_token_to(return_to_url, user, remote)
117     end
118     redirect_to @redirect_to
119   end
120
121   # Omniauth failure callback
122   def failure
123     flash[:notice] = params[:message]
124   end
125
126   # logout - Clear our rack session BUT essentially redirect to the provider
127   # to clean up the Devise session from there too !
128   def logout
129     session[:user_id] = nil
130
131     flash[:notice] = 'You have logged off'
132     return_to = params[:return_to] || root_url
133     redirect_to "#{Rails.configuration.Services.SSO.ExternalURL}/users/sign_out?redirect_uri=#{CGI.escape return_to}"
134   end
135
136   # login - Just bounce to /auth/joshid. The only purpose of this function is
137   # to save the return_to parameter (if it exists; see the application
138   # controller). /auth/joshid bypasses the application controller.
139   def login
140     if params[:remote] !~ /^[0-9a-z]{5}$/ && !params[:remote].nil?
141       return send_error 'Invalid remote cluster id', status: 400
142     end
143     if current_user and params[:return_to]
144       # Already logged in; just need to send a token to the requesting
145       # API client.
146       #
147       # FIXME: if current_user has never authorized this app before,
148       # ask for confirmation here!
149
150       return send_api_token_to(params[:return_to], current_user, params[:remote])
151     end
152     p = []
153     p << "auth_provider=#{CGI.escape(params[:auth_provider])}" if params[:auth_provider]
154
155     if !Rails.configuration.Login.LoginCluster.empty? and Rails.configuration.Login.LoginCluster != Rails.configuration.ClusterID
156       cluster = Rails.configuration.RemoteClusters[Rails.configuration.Login.LoginCluster]
157       if not cluster
158         raise "LoginCluster #{Rails.configuration.Login.LoginCluster} missing from RemoteClusters"
159       end
160       scheme = "https"
161       if cluster['Scheme'] and !cluster['Scheme'].empty?
162         scheme = cluster['Scheme']
163       end
164       if !cluster['Host'] or cluster['Host'].empty?
165         raise "LoginCluster #{Rails.configuration.Login.LoginCluster} missing 'Host' in RemoteClusters"
166       end
167       login_cluster = "#{scheme}://#{cluster['Host']}"
168       p << "remote=#{CGI.escape(params[:remote])}" if params[:remote]
169       p << "return_to=#{CGI.escape(params[:return_to])}" if params[:return_to]
170       redirect_to "#{login_cluster}/login?#{p.join('&')}"
171     else
172       if params[:return_to]
173         # Encode remote param inside callback's return_to, so that we'll get it on
174         # create() after login.
175         remote_param = params[:remote].nil? ? '' : params[:remote]
176         p << "return_to=#{CGI.escape(remote_param + ',' + params[:return_to])}"
177       end
178       redirect_to "/auth/joshid?#{p.join('&')}"
179     end
180   end
181
182   def send_api_token_to(callback_url, user, remote=nil)
183     # Give the API client a token for making API calls on behalf of
184     # the authenticated user
185
186     # Stub: automatically register all new API clients
187     api_client_url_prefix = callback_url.match(%r{^.*?://[^/]+})[0] + '/'
188     act_as_system_user do
189       @api_client = ApiClient.
190         find_or_create_by(url_prefix: api_client_url_prefix)
191     end
192
193     @api_client_auth = ApiClientAuthorization.
194       new(user: user,
195           api_client: @api_client,
196           created_by_ip_address: remote_ip,
197           scopes: ["all"])
198     @api_client_auth.save!
199
200     if callback_url.index('?')
201       callback_url += '&'
202     else
203       callback_url += '?'
204     end
205     if remote.nil?
206       token = @api_client_auth.token
207     else
208       token = @api_client_auth.salted_token(remote: remote)
209     end
210     callback_url += 'api_token=' + token
211     redirect_to callback_url
212   end
213
214   def cross_origin_forbidden
215     send_error 'Forbidden', status: 403
216   end
217 end