15881: Add LDAP authentication option.
[arvados.git] / services / api / app / controllers / user_sessions_controller.rb
index c5507045c42e222ba3bd09ae08b41d45bccd52b8..200260bce232b45c337b4b2f1a956eed4293bf22 100644 (file)
@@ -1,77 +1,64 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
 class UserSessionsController < ApplicationController
-  before_filter :require_auth_scope, :only => [ :destroy ]
+  before_action :require_auth_scope, :only => [ :destroy ]
 
-  skip_before_filter :set_cors_headers
-  skip_before_filter :find_object_by_uuid
-  skip_before_filter :render_404_if_no_object
+  skip_before_action :set_cors_headers
+  skip_before_action :find_object_by_uuid
+  skip_before_action :render_404_if_no_object
 
   respond_to :html
 
   # omniauth callback method
   def create
-    omniauth = env['omniauth.auth']
-
-    identity_url_ok = (omniauth['info']['identity_url'].length > 0) rescue false
-    unless identity_url_ok
-      # Whoa. This should never happen.
-      logger.error "UserSessionsController.create: omniauth object missing/invalid"
-      logger.error "omniauth: "+omniauth.pretty_inspect
-
-      return redirect_to login_failure_url
+    if !Rails.configuration.Login.LoginCluster.empty? and Rails.configuration.Login.LoginCluster != Rails.configuration.ClusterID
+      raise "Local login disabled when LoginCluster is set"
     end
 
-    user = User.find_by_identity_url(omniauth['info']['identity_url'])
-    if not user
-      # Check for permission to log in to an existing User record with
-      # a different identity_url
-      Link.where("link_class = ? and name = ? and tail_uuid = ? and head_uuid like ?",
-                 'permission',
-                 'can_login',
-                 omniauth['info']['email'],
-                 User.uuid_like_pattern).each do |link|
-        if prefix = link.properties['identity_url_prefix']
-          if prefix == omniauth['info']['identity_url'][0..prefix.size-1]
-            user = User.find_by_uuid(link.head_uuid)
-            break if user
-          end
-        end
-      end
-    end
-    if not user
-      # New user registration
-      user = User.new(:email => omniauth['info']['email'],
-                      :first_name => omniauth['info']['first_name'],
-                      :last_name => omniauth['info']['last_name'],
-                      :identity_url => omniauth['info']['identity_url'],
-                      :is_active => Rails.configuration.new_users_are_active,
-                      :owner_uuid => system_user_uuid)
-      if omniauth['info']['username']
-        user.set_initial_username(requested: omniauth['info']['username'])
-      end
-      act_as_system_user do
-        user.save or raise Exception.new(user.errors.messages)
+    if params[:provider] == 'controller'
+      if request.headers['Authorization'] != 'Bearer ' + Rails.configuration.SystemRootToken
+        return send_error('Invalid authorization header', status: 401)
       end
+      # arvados-controller verified the user and is passing auth_info
+      # in request params.
+      authinfo = SafeJSON.load(params[:auth_info])
     else
-      user.email = omniauth['info']['email']
-      user.first_name = omniauth['info']['first_name']
-      user.last_name = omniauth['info']['last_name']
-      if user.identity_url.nil?
-        # First login to a pre-activated account
-        user.identity_url = omniauth['info']['identity_url']
-      end
+      # omniauth middleware verified the user and is passing auth_info
+      # in request.env.
+      authinfo = request.env['omniauth.auth']['info'].with_indifferent_access
+    end
+
+    Rails.logger.warn "authinfo was #{authinfo.inspect}"
+
+    begin
+      user = User.register(authinfo)
+    rescue => e
+      Rails.logger.warn "User.register error #{e}"
+      Rails.logger.warn "authinfo was #{authinfo.inspect}"
+      return redirect_to login_failure_url
     end
 
     # For the benefit of functional and integration tests:
     @user = user
 
+    if user.uuid[0..4] != Rails.configuration.ClusterID
+      # Actually a remote user
+      # Send them to their home cluster's login
+      rh = Rails.configuration.RemoteClusters[user.uuid[0..4]]
+      remote, return_to_url = params[:return_to].split(',', 2)
+      @remotehomeurl = "#{rh.Scheme || "https"}://#{rh.Host}/login?remote=#{Rails.configuration.ClusterID}&return_to=#{return_to_url}"
+      render
+      return
+    end
+
     # prevent ArvadosModel#before_create and _update from throwing
     # "unauthorized":
     Thread.current[:user] = user
 
     user.save or raise Exception.new(user.errors.messages)
 
-    omniauth.delete('extra')
-
     # Give the authenticated user a cookie for direct API access
     session[:user_id] = user.id
     session[:api_client_uuid] = nil
@@ -79,7 +66,15 @@ class UserSessionsController < ApplicationController
 
     @redirect_to = root_path
     if params.has_key?(:return_to)
-      return send_api_token_to(params[:return_to], user)
+      # return_to param's format is 'remote,return_to_url'. This comes from login()
+      # encoding the remote=zbbbb parameter passed by a client asking for a salted
+      # token.
+      remote, return_to_url = params[:return_to].split(',', 2)
+      if remote !~ /^[0-9a-z]{5}$/ && remote != ""
+        return send_error 'Invalid remote cluster id', status: 400
+      end
+      remote = nil if remote == ''
+      return send_api_token_to(return_to_url, user, remote)
     end
     redirect_to @redirect_to
   end
@@ -96,15 +91,16 @@ class UserSessionsController < ApplicationController
 
     flash[:notice] = 'You have logged off'
     return_to = params[:return_to] || root_url
-    redirect_to "#{Rails.configuration.sso_provider_url}/users/sign_out?redirect_uri=#{CGI.escape return_to}"
+    redirect_to "#{Rails.configuration.Services.SSO.ExternalURL}/users/sign_out?redirect_uri=#{CGI.escape return_to}"
   end
 
   # login - Just bounce to /auth/joshid. The only purpose of this function is
   # to save the return_to parameter (if it exists; see the application
   # controller). /auth/joshid bypasses the application controller.
   def login
-    auth_provider = if params[:auth_provider] then "auth_provider=#{CGI.escape(params[:auth_provider])}" else "" end
-
+    if params[:remote] !~ /^[0-9a-z]{5}$/ && !params[:remote].nil?
+      return send_error 'Invalid remote cluster id', status: 400
+    end
     if current_user and params[:return_to]
       # Already logged in; just need to send a token to the requesting
       # API client.
@@ -112,15 +108,37 @@ class UserSessionsController < ApplicationController
       # FIXME: if current_user has never authorized this app before,
       # ask for confirmation here!
 
-      send_api_token_to(params[:return_to], current_user)
-    elsif params[:return_to]
-      redirect_to "/auth/joshid?return_to=#{CGI.escape(params[:return_to])}&#{auth_provider}"
+      return send_api_token_to(params[:return_to], current_user, params[:remote])
+    end
+    p = []
+    p << "auth_provider=#{CGI.escape(params[:auth_provider])}" if params[:auth_provider]
+
+    if !Rails.configuration.Login.LoginCluster.empty? and Rails.configuration.Login.LoginCluster != Rails.configuration.ClusterID
+      host = ApiClientAuthorization.remote_host(uuid_prefix: Rails.configuration.Login.LoginCluster)
+      if not host
+        raise "LoginCluster #{Rails.configuration.Login.LoginCluster} missing from RemoteClusters"
+      end
+      scheme = "https"
+      cluster = Rails.configuration.RemoteClusters[Rails.configuration.Login.LoginCluster]
+      if cluster and cluster['Scheme'] and !cluster['Scheme'].empty?
+        scheme = cluster['Scheme']
+      end
+      login_cluster = "#{scheme}://#{host}"
+      p << "remote=#{CGI.escape(params[:remote])}" if params[:remote]
+      p << "return_to=#{CGI.escape(params[:return_to])}" if params[:return_to]
+      redirect_to "#{login_cluster}/login?#{p.join('&')}"
     else
-      redirect_to "/auth/joshid?#{auth_provider}"
+      if params[:return_to]
+        # Encode remote param inside callback's return_to, so that we'll get it on
+        # create() after login.
+        remote_param = params[:remote].nil? ? '' : params[:remote]
+        p << "return_to=#{CGI.escape(remote_param + ',' + params[:return_to])}"
+      end
+      redirect_to "/auth/joshid?#{p.join('&')}"
     end
   end
 
-  def send_api_token_to(callback_url, user)
+  def send_api_token_to(callback_url, user, remote=nil)
     # Give the API client a token for making API calls on behalf of
     # the authenticated user
 
@@ -131,19 +149,24 @@ class UserSessionsController < ApplicationController
         find_or_create_by(url_prefix: api_client_url_prefix)
     end
 
-    api_client_auth = ApiClientAuthorization.
+    @api_client_auth = ApiClientAuthorization.
       new(user: user,
           api_client: @api_client,
           created_by_ip_address: remote_ip,
           scopes: ["all"])
-    api_client_auth.save!
+    @api_client_auth.save!
 
     if callback_url.index('?')
       callback_url += '&'
     else
       callback_url += '?'
     end
-    callback_url += 'api_token=' + api_client_auth.api_token
+    if remote.nil?
+      token = @api_client_auth.token
+    else
+      token = @api_client_auth.salted_token(remote: remote)
+    end
+    callback_url += 'api_token=' + token
     redirect_to callback_url
   end