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