lib/zuora_connect/controllers/helpers.rb



require 'apartment/migrator'
module ZuoraConnect
  module Controllers
    module Helpers
      extend ActiveSupport::Concern

      def authenticate_app_api_request
        #Skip session for api requests
        Thread.current[:appinstance] = nil
        request.session_options[:skip] = true
        if defined?(ElasticAPM) && ElasticAPM.running?
          if ElasticAPM.respond_to?(:set_label)
            ElasticAPM.set_label(:trace_id, request.uuid) if defined?(ElasticAPM) && ElasticAPM.running?
          else
            ElasticAPM.set_label(:trace_id, request.uuid) if defined?(ElasticAPM) && ElasticAPM.running?
          end
        end
        start_time = Time.now
        if request.headers["API-Token"].present?
          @appinstance = ZuoraConnect::AppInstance.where(:api_token => request.headers["API-Token"]).first
          ZuoraConnect.logger.debug("[#{@appinstance.id}] API REQUEST - API token") if @appinstance.present?
          check_instance
        elsif ZuoraConnect::AppInstance::INTERNAL_HOSTS.include?(request.headers.fetch("HOST", nil))
          zuora_host, zuora_entity_id, zuora_instance_id = [request.headers['zuora-host'], (request.headers['zuora-entity-ids'] || "").gsub('-',''), request.headers['zuora-instance-id']]

          #Validate host present
          if zuora_host.blank?
            render json: {"status": 401, "message": "zuora-host header was not supplied."}, status: :unauthorized
            return
          end
          #Validate entity-ids present
          if zuora_entity_id.blank?
            render json: {"status": 401, "message": "zuora-entity-ids header was not supplied."}, status: :unauthorized
            return
          end
          #Select with instance id if present. Used where mulitple deployments are done.
          if zuora_instance_id.present?
            appinstances = ZuoraConnect::AppInstance.where("zuora_entity_ids ?& array[:entities] = true AND zuora_domain = :host AND id = :id", entities: [zuora_entity_id], host: zuora_host, id: zuora_instance_id.to_i)
          else
            appinstances = ZuoraConnect::AppInstance.where("zuora_entity_ids ?& array[:entities] = true AND zuora_domain = :host", entities: [zuora_entity_id], host: zuora_host)
          end

          if appinstances.size == 0
            render json: {"status": 401, "message": "Missing mapping or no deployment for '#{zuora_host}-#{zuora_entity_id}' ."}, status: :unauthorized
            return
          elsif appinstances.size > 1
            render json: {"status": 401, "message": "More than one app instance binded to host and entity ids. Please indicate correct instance via 'zuora-instance-id' header"}, status: :unauthorized
            return
          else
            @appinstance = appinstances.first
            check_instance
          end
      
        elsif request.headers.fetch("Authorization", "").include?("Basic ")
          authenticate_or_request_with_http_basic do |username, password|
            @appinstance = ZuoraConnect::AppInstance.where(:token => password).first
            @appinstance ||= ZuoraConnect::AppInstance.where(:api_token => password).first
            ZuoraConnect.logger.debug("[#{@appinstance.id}] API REQUEST - Basic Auth") if @appinstance.present?
            check_instance
          end
        else
          check_instance
        end
        
        if @appinstance.present?
          ZuoraConnect.logger.debug("[#{@appinstance.id}] Authenticate App API Request Completed In - #{(Time.now - start_time).round(2)}s")
        end
      end

       #API ONLY
      def check_instance
        if defined?(@appinstance) && @appinstance.present?
          if @appinstance.new_session_for_api_requests(:params => params)
            @appinstance.new_session(:session => @appinstance.data_lookup(:session => session))
          end
          Thread.current[:appinstance] = @appinstance
          PaperTrail.whodunnit = "API User" if defined?(PaperTrail)
          ElasticAPM.set_user("API User")  if defined?(ElasticAPM) && ElasticAPM.running?
          return true
        else
          response.set_header('WWW-Authenticate', "Basic realm=\"Application\"")
          render json: {"status": 401, "message": "Access Denied"}, status: :unauthorized
          return false
        end
      end

      def authenticate_connect_app_request
        if defined?(ElasticAPM) && ElasticAPM.running?
          if ElasticAPM.respond_to?(:set_label)
            ElasticAPM.set_label(:trace_id, request.uuid)
          else
            ElasticAPM.set_label(:trace_id, request.uuid)
          end
        end
        Thread.current[:appinstance] = nil
        start_time = Time.now

        if ZuoraConnect.configuration.mode == "Production"
          zuora_entity_id = request.headers['ZuoraCurrentEntity'] || cookies['ZuoraCurrentEntity']

          if zuora_entity_id.present?
            zuora_tenant_id = cookies['Zuora-Tenant-Id']
            zuora_user_id = cookies['Zuora-User-Id']
            zuora_host = request.headers["HTTP_X_FORWARDED_HOST"] || "apisandbox.zuora.com"

            zuora_details = {'host' => zuora_host, 'user_id' => zuora_user_id, 'tenant_id' => zuora_tenant_id, 'entity_id' => zuora_entity_id}

            #Do we need to refresh session identity
            if request.headers["Zuora-Auth-Token"].present?
              zuora_client = ZuoraAPI::Oauth.new(url: "https://#{zuora_host}", bearer_token: request.headers["Zuora-Auth-Token"], oauth_session_expires_at: Time.now + 5.minutes )
            elsif cookies['ZSession'].present?
              zuora_client = ZuoraAPI::Basic.new(url: "https://#{zuora_host}", session: cookies['ZSession'])
            else
              render "zuora_connect/static/error_handled", :locals => {
                :title => "Missing Authorization Token", 
                :message => "Zuora 'Zuora-Auth-Token' header and 'ZSession' cookie not present."
              }, :layout => false
              return
            end

            begin
              zuora_instance_id = params[:sidebar_launch].to_s.to_bool ? nil : (params[:app_instance_id] || session["appInstance"])

              #Identity blank or current entity different
              different_zsession = session["ZSession"] != cookies['ZSession']
              missmatched_entity = session["ZuoraCurrentEntity"] != zuora_entity_id
              missing_identity = session["ZuoraCurrentIdentity"].blank?

              if (missing_identity || missmatched_entity || different_zsession)
                zuora_details.merge!({'identity' => {'different_zsession' => different_zsession, 'missing_identity' => missing_identity, 'missmatched_entity' => missmatched_entity}})
                identity, response = zuora_client.rest_call(url: zuora_client.rest_endpoint("identity"))
                session["ZuoraCurrentIdentity"] = identity
                session["ZuoraCurrentEntity"] = identity['entityId']
                session["ZSession"] = cookies['ZSession']
                zuora_instance_id = nil
                zuora_details["identity"]["entityId"] = identity['entityId']

                client_describe, response = zuora_client.rest_call(url: zuora_client.rest_endpoint("genesis/user/info").gsub('v1/', ''), session_type: zuora_client.class == ZuoraAPI::Oauth ? :bearer : :basic, headers: zuora_client.class == ZuoraAPI::Oauth ? {} : {'Authorization' => "ZSession-a3N2w #{zuora_client.get_session(prefix: false, auth_type: :basic)}"})
                session["ZuoraCurrentUserInfo"] = client_describe
              
                raise ZuoraConnect::Exceptions::Error.new("Header entity id does not match identity call entity id.") if zuora_entity_id != identity['entityId']
              end

              #Find matching app instances.
              if zuora_instance_id.present?
                appinstances = ZuoraConnect::AppInstance.where("zuora_entity_ids ?& array[:entities] = true AND zuora_domain = :host AND id = :id", entities: [zuora_entity_id], host: zuora_client.rest_domain, id: zuora_instance_id.to_i).pluck(:id, :name)
              else
                #if app_instance_ids is present then permissions still controlled by connect
                if params[:app_instance_ids].present? 
                  navbar, response = zuora_client.rest_call(url: zuora_client.rest_endpoint("navigation"))
                  urls = navbar['menus'].map {|x| x['url']}
                  app_env = ENV["DEIS_APP"] || "xyz123"
                  url = urls.compact.select {|url| File.basename(url).start_with?(app_env + '?')}.first
                  begin
                    task_ids = JSON.parse(Base64.urlsafe_decode64(CGI.parse(URI.parse(url).query)["app_instance_ids"][0]))
                  rescue URI::InvalidURIError => ex
                    raise ZuoraConnect::Exceptions::APIError.new(message: "Failure in parsing the navbar urls.", response: response)
                  end
                  appinstances = ZuoraConnect::AppInstance.where(:id => task_ids).pluck(:id, :name)
                else
                  appinstances = ZuoraConnect::AppInstance.where("zuora_entity_ids ?& array[:entities] = true AND zuora_domain = :host", entities: [zuora_entity_id], host: zuora_client.rest_domain).pluck(:id, :name)
                end
              end

              zuora_user_id = cookies['Zuora-User-Id'] || session["ZuoraCurrentIdentity"]['userId']

              #One deployed instance
              if appinstances.size == 1
                ZuoraConnect.logger.debug("Instance is #{appinstances.to_h.keys.first}")
                @appinstance = ZuoraConnect::AppInstance.find(appinstances.to_h.keys.first)

                #Add user/update 
                begin
                  @zuora_user = ZuoraConnect::ZuoraUser.where(:zuora_user_id => zuora_user_id).first
                rescue ActiveRecord::StatementInvalid => ex
                  if ex.message.include?("PG::UndefinedTable") && ex.message.include?("zuora_users")
                    self.apartment_switch(nil,true)
                    retry
                  else
                    raise
                  end
                end
                if @zuora_user.present?
                  ZuoraConnect.logger.debug("Current zuora user #{zuora_user_id}")
                  if @zuora_user.updated_at < Time.now - 1.day
                    @zuora_user.zuora_identity_response[zuora_entity_id] = session["ZuoraCurrentIdentity"]
                    @zuora_user.save!
                  end
                else
                  ZuoraConnect.logger.debug("New zuora user object for #{zuora_user_id}")
                  @zuora_user = ZuoraConnect::ZuoraUser.create!(:zuora_user_id => zuora_user_id, :zuora_identity_response => {zuora_entity_id => session["ZuoraCurrentIdentity"]})
                end 
                @zuora_user.session = session
                session["#{@appinstance.id}::user::localUserId"] = @zuora_user.id
                session["#{@appinstance.id}::user::email"] = session['ZuoraCurrentIdentity']["username"]
                session["#{@appinstance.id}::user::timezone"] = session['ZuoraCurrentIdentity']["timeZone"]
                session["#{@appinstance.id}::user::locale"] = session['ZuoraCurrentIdentity']["language"]
                session["appInstance"] = @appinstance.id

              #We have multiple, user must pick
              elsif appinstances.size > 1 
                ZuoraConnect.logger.debug("User must select instance. #{@names}")
                render "zuora_connect/static/launch", :locals => {:names => appinstances.to_h}, :layout => false
                return

              #We have no deployed instance for this tenant
              else 
                #Ensure user can access oauth creation API 
                if session["ZuoraCurrentIdentity"]['platformRole'] != 'ADMIN' 
                  Thread.current[:appinstance] = nil
                  session["appInstance"] = nil
                  render "zuora_connect/static/error_handled", :locals => {
                    :title => "Application can only complete its initial setup via platform administrator", 
                    :message => "Please contact admin of tenant and have them click on link again to launch application."
                  }, :layout => false
                  return
                end
                Apartment::Tenant.switch!("public")
                ActiveRecord::Base.transaction do
                  ActiveRecord::Base.connection.execute('LOCK public.zuora_users IN ACCESS EXCLUSIVE MODE')
                  appinstances = ZuoraConnect::AppInstance.where("zuora_entity_ids ?& array[:entities] = true AND zuora_domain = :host", entities: [zuora_entity_id], host: zuora_client.rest_domain).pluck(:id, :name)

                  if appinstances.size > 0
                    redirect_to "https://#{zuora_host}/apps/newlogin.do?retURL=#{request.fullpath}"
                    return
                  end

                  next_id = (ZuoraConnect::AppInstance.all.where('id > 24999999').order(id: :desc).limit(1).pluck(:id).first || 24999999) + 1
                  user = (ENV['DEIS_APP'] || "Application").split('-').map(&:capitalize).join(' ')
                  body = {
                    'userId' => zuora_user_id, 
                    'entityIds' => [zuora_entity_id.unpack("a8a4a4a4a12").join('-')], 
                    'customAuthorities' => [], 
                    'additionalInformation' => {
                      'description' => "This user is for #{user} application.", 
                      'name' => "#{user} API User #{next_id}"
                    }
                  }

                  oauth_response, response = zuora_client.rest_call(method: :post, body: body.to_json, url: zuora_client.rest_endpoint("genesis/clients").gsub('v1/', ''), session_type: zuora_client.class == ZuoraAPI::Oauth ? :bearer : :basic, headers: zuora_client.class == ZuoraAPI::Oauth ? {} : {'Authorization' => "ZSession-a3N2w #{zuora_client.get_session(prefix: false, auth_type: :basic)}"})

                  new_zuora_client = ZuoraAPI::Oauth.new(url: "https://#{zuora_host}", oauth_client_id: oauth_response["clientId"], oauth_secret: oauth_response["clientSecret"] )
                  if session["ZuoraCurrentUserInfo"].blank?
                    client_describe, response = new_zuora_client.rest_call(url: zuora_client.rest_endpoint("genesis/user/info").gsub('v1/', ''), session_type: :bearer)
                  else
                    client_describe = session["ZuoraCurrentUserInfo"]
                  end

                  available_entities = client_describe["accessibleEntities"].select {|entity| entity['id'] == zuora_entity_id}
                  task_data = {
                    "id": next_id,
                    "name": client_describe["tenantName"],
                    "mode": "Collections",
                    "status": "Running",
                    ZuoraConnect::AppInstance::LOGIN_TENANT_DESTINATION => {
                      "tenant_type": "Zuora",
                      "username": session["ZuoraCurrentIdentity"]["username"],
                      "url": new_zuora_client.url,
                      "status": "Active",
                      "oauth_client_id": oauth_response['clientId'],
                      "oauth_secret": oauth_response['clientSecret'],
                      "authentication_type": "OAUTH",
                      "entities": available_entities.map {|e| e.merge({'displayName' => client_describe["tenantName"]})}
                    },
                    "tenant_ids": available_entities.map{|e| e['entityId']}.uniq,
                  }
                  mapped_values = {:id => next_id, :api_token => rand(36**64).to_s(36), :token => rand(36**64).to_s(36), :zuora_logins => task_data, :oauth_expires_at => Time.now + 1000.years, :zuora_domain => zuora_client.rest_domain, :zuora_entity_ids => [zuora_entity_id]}
                  @appinstance = ZuoraConnect::AppInstance.new(mapped_values)
                  retry_count = 0
                  begin
                    @appinstance.save(:validate => false)
                  rescue ActiveRecord::RecordNotUnique => ex
                    if (retry_count += 1) < 3
                      @appinstance.assign_attributes({:api_token => rand(36**64).to_s(36), :token => rand(36**64).to_s(36)})
                      retry
                    else
                      Thread.current[:appinstance] = nil
                      session["appInstance"] = nil
                      render "zuora_connect/static/error_handled", :locals => {
                        :title => "Application could not create unique tokens.", 
                        :message => "Please contact support or retry launching application."
                      }, :layout => false
                      return
                    end
                  end
                end

                Apartment::Tenant.switch!("public")
                begin
                  Apartment::Tenant.create(@appinstance.id.to_s)
                rescue Apartment::TenantExists => ex
                  ZuoraConnect.logger.debug("Tenant Already Exists")
                end
                @appinstance.refresh
                session["appInstance"] = @appinstance.id
              end

            rescue ZuoraAPI::Exceptions::ZuoraAPIAuthenticationTypeError => ex
              output_xml, input_xml, response = zuora_client.soap_call(errors: [], z_session: false) do |xml|
                xml['api'].getUserInfo
              end
              final_error = output_xml.xpath('//fns:FaultCode', 'fns' =>'http://fault.api.zuora.com/').text
              session.clear
              if final_error.blank?
                ZuoraConnect.logger.warn("UI Authorization Error", ex, zuora: zuora_details.merge({:error => response.body}))
              elsif final_error != "INVALID_SESSION"
                ZuoraConnect.logger.warn("UI Authorization Error", ex, zuora: zuora_details.merge({:error => final_error}))
              end
              redirect_to "https://#{zuora_host}/apps/newlogin.do?retURL=#{request.fullpath}"
              return
            rescue => ex
              if defined?(ex.response) && ex.response.present? && defined?(ex.response.body)
                zuora_details.merge!({:error => ex.response.body})
              end
              ZuoraConnect.logger.error("UI Authorization Error", ex, zuora: zuora_details)
              render "zuora_connect/static/error_unhandled", :locals => {:exception => ex}, :layout => false
              return              
            end
          elsif request["data"] && /^([A-Za-z0-9+\/\-\_]{4})*([A-Za-z0-9+\/]{4}|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{2}==)$/.match(request["data"].to_s)
            setup_instance_via_data
          else
            if session["appInstance"].present?
              @appinstance = ZuoraConnect::AppInstance.where(:id => session["appInstance"]).first 
            else
              render "zuora_connect/static/error_handled", :locals => {
                :title => "Application state could not be verified", 
                :message => "Please relaunch application."
              }, :layout => false
              return 
            end
          end
        else
          setup_instance_via_dev_mode
        end

        if !defined?(@appinstance) || @appinstance.blank?
          render "zuora_connect/static/error_handled", :locals => {
            :title => "Application state could not be found.", 
            :message => "Please relaunch application."
          }, :layout => false
          return 
        end
        #Call .data_lookup with the current session to retrieve session. In some cases session may be stored/cache in redis 
        #so data lookup provides a model method that can be overriden per app.
        if params[:controller] != 'zuora_connect/api/v1/app_instance' && params[:action] != 'drop'
          if @appinstance.new_session_for_ui_requests(:params => params)
            @appinstance.new_session(:session => @appinstance.data_lookup(:session => session))
          end
        end

        if session["#{@appinstance.id}::user::email"].present? 
          ElasticAPM.set_user(session["#{@appinstance.id}::user::email"])  if defined?(ElasticAPM) && ElasticAPM.running?
          PaperTrail.whodunnit =  session["#{@appinstance.id}::user::email"] if defined?(PaperTrail)
        end
        begin
          locale = session["#{@appinstance.id}::user::locale"]
          I18n.locale = locale.present? ? locale : @appinstance.locale
        rescue I18n::InvalidLocale => ex
          ZuoraConnect.logger.error(ex) if !ZuoraConnect::AppInstance::IGNORED_LOCALS.include?(ex.locale.to_s.downcase)
        end
        begin
          Time.zone = session["#{@appinstance.id}::user::timezone"] ? session["#{@appinstance.id}::user::timezone"] : @appinstance.timezone
        rescue 
          ZuoraConnect.logger.error(ex) 
        end

        ZuoraConnect.logger.debug("[#{@appinstance.blank? ? "N/A" : @appinstance.id}] Authenticate App Request Completed In - #{(Time.now - start_time).round(2)}s")

      rescue ZuoraConnect::Exceptions::InvalidCredentialSet => ex
        id = @appinstance.id
        ZuoraConnect::AppInstance.destroy(id)
        Apartment::Tenant.drop(id)
        render "zuora_connect/static/error_handled", :locals => {
          :title => "Application Setup Error", 
          :message => "Application cannot be run using Zuora Session. Delete old application \
          deployment and create new with Zuora Basic or OAuth credentials."
        }, :layout => false
        return 
      rescue => ex
        ZuoraConnect.logger.error("UI Authorization Error", ex)
        respond_to do |format|
          format.html { render 'zuora_connect/static/error_unhandled', :locals => {:exception => ex, :skip_exception => true}, status: 500, layout: false }
          format.js { render 'zuora_connect/static/error_unhandled', :locals => {:exception => ex, :skip_exception => true}, status: 500, layout: false}
        end
        return
      end

      def persist_connect_app_session
        if @appinstance.present?
          if defined?(Redis.current)
            @appinstance.cache_app_instance
          else
            session.merge!(@appinstance.save_data)
          end
        end
      end

      def check_connect_admin!
        raise ZuoraConnect::Exceptions::AccessDenied.new("User is not an authorized admin for this application") if !session["#{@appinstance.id}::admin"]
      end

      def check_connect_admin
        return session["#{@appinstance.id}::admin"]
      end

      def zuora_user
        return @zuora_user
      end

      def hallway_integration?
        return (request.headers['ZuoraCurrentEntity'].present? || cookies['ZuoraCurrentEntity'].present?)
      end

    private
      def setup_instance_via_data
        session.clear
        values = JSON.parse(ZuoraConnect::AppInstance.decrypt_response(Base64.urlsafe_decode64(request["data"])))
        if values["param_data"]
          values["param_data"].each do |k ,v|
            params[k] = v
          end
        end
        session["#{values["appInstance"]}::destroy"] = values["destroy"]
        session["appInstance"] = values["appInstance"]
        if values["current_user"]
          session["#{values["appInstance"]}::admin"] = values["current_user"]["admin"] ? values["current_user"]["admin"] : false
          session["#{values["appInstance"]}::user::timezone"] = values["current_user"]["timezone"]
          session["#{values["appInstance"]}::user::locale"] = values["current_user"]["locale"]
          session["#{values["appInstance"]}::user::email"] = values["current_user"]["email"]
        end

        ZuoraConnect.logger.debug({msg: 'Setup values', connect: values}) if Rails.env != "production"

        @appinstance = ZuoraConnect::AppInstance.where(:id => values["appInstance"].to_i).first

        if @appinstance.blank?
          Apartment::Tenant.switch!("public")
          begin
            Apartment::Tenant.create(values["appInstance"].to_s)
          rescue Apartment::TenantExists => ex
            ZuoraConnect.logger.debug("Tenant Already Exists")
          end
          mapped_values = {:api_token => values['api_token'], :token => values['api_token'], :access_token => values["access_token"], :refresh_token => values["refresh_token"], :oauth_expires_at => values["expires"]}
          @appinstance = ZuoraConnect::AppInstance.new(mapped_values.merge({:id => values["appInstance"].to_i}))
          @appinstance.save(:validate => false)
        else
          mapped_values = {:access_token => values["access_token"], :refresh_token => values["refresh_token"], :oauth_expires_at => values["expires"]}
          @appinstance.assign_attributes(mapped_values)
          if @appinstance.access_token_changed? && @appinstance.refresh_token_changed?
            @appinstance.save(:validate => false)
          else
            raise ZuoraConnect::Exceptions::AccessDenied.new("Authorization mismatch. Possible tampering")
          end
        end     
      end

      def setup_instance_via_dev_mode
        session["appInstance"] = ZuoraConnect.configuration.dev_mode_appinstance
        user = ZuoraConnect.configuration.dev_mode_user
        key = ZuoraConnect.configuration.dev_mode_pass
        values = {:user => user , :key => key, :appinstance => session["appInstance"]}
        @appinstance = ZuoraConnect::AppInstance.where(:id => values[:appinstance].to_i).first
        if @appinstance.blank?
          Apartment::Tenant.switch!("public")
          begin
            Apartment::Tenant.create(values[:appinstance].to_s)
          rescue Apartment::TenantExists => ex
            Apartment::Tenant.drop(values[:appinstance].to_s)
            retry
          end

          @appinstance = ZuoraConnect::AppInstance.new(:id => values[:appinstance].to_i, :access_token => values[:user], :refresh_token => values[:key], :token => "#{values[:key]}#{values[:key]}", :api_token => "#{values[:key]}#{values[:key]}")
          @appinstance.save(:validate => false)
        end
        if @appinstance.access_token.blank? || @appinstance.refresh_token.blank? || @appinstance.token.blank? || @appinstance.api_token.blank?
          @appinstance.update_attributes!(:access_token =>  values["user"], :refresh_token =>  values["key"], :token => "#{values[:key]}#{values[:key]}", :api_token => "#{values[:key]}#{values[:key]}")
        end
        session["#{@appinstance.id}::admin"] =  ZuoraConnect.configuration.dev_mode_admin
      end
    end
  end
end