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
ElasticAPM.set_tag(:trace_id, request.uuid) if defined?(ElasticAPM) && ElasticAPM.running?
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)
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
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
else
@appinstance = appinstances.first
end
else #if 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
def verify_with_navbar
if !session[params[:app_instance_ids]].present?
host = request.headers["HTTP_X_FORWARDED_HOST"]
zuora_client = ZuoraAPI::Login.new(url: "https://#{host}")
menus = zuora_client.get_full_nav(cookies.to_h)["menus"]
app = menus.select do |item|
matches = /(?<=.com\/services\/)(.*?)(?=\?|$)/.match(item["url"])
if !matches.blank?
matches[0].split("?").first == ENV["DEIS_APP"]
end
end
session[params[:app_instance_ids]] = app[0]
return app[0]
else
return session[params[:app_instance_ids]]
end
end
def select_instance
begin
app = verify_with_navbar
url_tasks = JSON.parse(Base64.urlsafe_decode64(CGI.parse(URI.parse(app["url"]).query)["app_instance_ids"][0]))
@app_instance_ids = JSON.parse(Base64.urlsafe_decode64(params[:app_instance_ids]))
if (url_tasks & @app_instance_ids).size == @app_instance_ids.size
sql = "select name,id from zuora_connect_app_instances where id = ANY(ARRAY#{@app_instance_ids})"
result = ActiveRecord::Base.connection.execute(sql)
@names = {}
result.each do |x|
@names[x["id"].to_i] = x["name"]
end
render "zuora_connect/static/launch"
else
render "zuora_connect/static/invalid_launch_request"
end
rescue => ex
ZuoraConnect.logger.debug("Error parsing Instance ID's: #{ex.message}")
render "zuora_connect/static/invalid_launch_request"
end
end
def authenticate_connect_app_request
ElasticAPM.set_tag(:trace_id, request.uuid) if defined?(ElasticAPM) && ElasticAPM.running?
Thread.current[:appinstance] = nil
if request.headers['Zuora-Auth-Token'].present? && request.headers["NEWZUORA"].present?
#Debug
headers = request.headers.env.select do |k, _|
k.downcase.start_with?('http') ||
k.in?(ActionDispatch::Http::Headers::CGI_VARIABLES)
end
puts headers
#Do we need to refresh session identity
zuora_host = request.headers["HTTP_X_FORWARDED_HOST"] || "apisandbox.zuora.com"
zuora_client = ZuoraAPI::Login.new(url: "https://#{zuora_host}", session: cookies['ZSession'])
zuora_entity_id = request.headers['ZuoraCurrentEntity']
zuora_instance_id = params[:sidebar_launch].to_bool ? nil : (params[:app_instance_id] || session["appInstance"])
#Identity blank or current entity different
if (session["ZuoraCurrentIdentity"].blank? || session["ZuoraCurrentEntity"] != zuora_entity_id)
begin
identity, response = zuora_client.rest_call(url: zuora_client.rest_endpoint("identity"))
session["ZuoraCurrentIdentity"] = identity
session["ZuoraCurrentEntity"] = identity['entityId']
raise "Header entity id does not match identity call entity id" if zuora_entity_id != identity['entityId']
rescue => ex
ZuoraConnect.logger.error(ex)
render "zuora_connect/static/invalid_launch_request"
return
end
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).pluck(:id, :name)
else
#if app_instance_ids is present then permissions still controlled by connect
if params[:app_instance_ids].present?
begin
navbar, response = zuora_client.rest_call(url: zuora_client.rest_endpoint("v1/navigation"))
urls = a['menus'].map {|x| x['url']}
app_env = ENV["DEIS_APP"] || "xyz123"
url = urls.compact.select {|url| File.basename(url).start_with?(app_env + '?')}.first
task_ids = JSON.parse(Base64.urlsafe_decode64(CGI.parse(URI.parse(url).query)["app_instance_ids"][0]))
appinstances = ZuoraConnect::AppInstance.where(:id => task_ids).pluck(:id, :name)
rescue => ex
ZuoraConnect.logger.error(ex)
render "zuora_connect/static/invalid_launch_request"
return
end
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
#One deployed instance
if appinstances.size == 1
ZuoraConnect.logger.debug("Instance is #{appinstances.to_h.keys.first}")
session["appInstance"] = appinstances.to_h.keys.first
#We have multiple, user must pick
elsif appinstances.size > 1
ZuoraConnect.logger.debug("User must select instance. #{@names}")
@names = appinstances.to_h
render "zuora_connect/static/launch"
return
else
#Create new app instance
raise "Do not support new instance creation right now."
end
elsif params[:app_instance_ids].present?
if !params[:app_instance_id].present?
begin
app_instance_ids = JSON.parse(Base64.urlsafe_decode64(params[:app_instance_ids]))
if app_instance_ids.length == 1
verify_with_navbar
instances = JSON.parse(Base64.urlsafe_decode64(CGI.parse(URI.parse(session[params[:app_instance_ids]]["url"]).query)["app_instance_ids"][0]))
if instances.include?(app_instance_ids[0])
@appinstance = ZuoraConnect::AppInstance.find(app_instance_ids[0])
@appinstance.new_session(session: {})
@appinstance.cache_app_instance
session["appInstance"] = app_instance_ids[0]
else
ZuoraConnect.logger.error("Launch Error: Param Instance didnt match session data")
render "zuora_connect/static/invalid_launch_request"
return
end
else
select_instance
return
end
rescue => ex
ZuoraConnect.logger.error(ex)
render "zuora_connect/static/invalid_launch_request"
return
end
elsif params[:app_instance_id].present?
begin
instances = JSON.parse(Base64.urlsafe_decode64(CGI.parse(URI.parse(session[params[:app_instance_ids]]["url"]).query)["app_instance_ids"][0]))
if instances.include?(params[:app_instance_id].to_i)
@appinstance = ZuoraConnect::AppInstance.find(params[:app_instance_id].to_i)
@appinstance.new_session(session: {})
@appinstance.cache_app_instance
session["appInstance"] = params[:app_instance_id].to_i
else
render "zuora_connect/static/invalid_launch_request"
return
end
rescue => ex
ZuoraConnect.logger.error(ex)
render "zuora_connect/static/invalid_launch_request"
return
end
end
end
start_time = Time.now
if ZuoraConnect.configuration.mode == "Production"
if 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
setup_instance_via_session
end
else
setup_instance_via_dev_mode
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
I18n.locale = session["#{@appinstance.id}::user::locale"] ? session["#{@appinstance.id}::user::locale"] : @appinstance.locale
rescue I18n::InvalidLocale => ex
ZuoraConnect.logger.error(ex) if !ZuoraConnect::AppInstance::IGNORED_LOCALS.include?(ex.locale.to_s.downcase)
end
Time.zone = session["#{@appinstance.id}::user::timezone"] ? session["#{@appinstance.id}::user::timezone"] : @appinstance.timezone
ZuoraConnect.logger.debug("[#{@appinstance.blank? ? "N/A" : @appinstance.id}] Authenticate App Request Completed In - #{(Time.now - start_time).round(2)}s")
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
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
@appinstance = ZuoraConnect::AppInstance.new(:api_token => values[:api_token],:id => values["appInstance"].to_i, :access_token => values["access_token"].blank? ? values["user"] : values["access_token"], :token => values["refresh_token"] , :refresh_token => values["refresh_token"].blank? ? values["key"] : values["refresh_token"], :oauth_expires_at => values["expires"])
@appinstance.save(:validate => false)
else
@appinstance.access_token = values["access_token"] if !values["access_token"].blank? && @appinstance.access_token != values["access_token"]
@appinstance.refresh_token = values["refresh_token"] if !values["refresh_token"].blank? && @appinstance.refresh_token != values["refresh_token"]
@appinstance.oauth_expires_at = values["expires"] if !values["expires"].blank?
@appinstance.api_token = values["api_token"] if !values["api_token"].blank? && @appinstance.api_token != values["api_token"]
if @appinstance.access_token_changed? && @appinstance.refresh_token_changed?
@appinstance.save(:validate => false)
else
raise ZuoraConnect::Exceptions::AccessDenied.new("Authorization mistmatch. Possible tampering")
end
end
end
def setup_instance_via_session
if session["appInstance"].present?
@appinstance = ZuoraConnect::AppInstance.where(:id => session["appInstance"]).first
else
raise ZuoraConnect::Exceptions::SessionInvalid.new("Session Blank -- Relaunch Application")
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
#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
render html: "HTTP Basic: Access denied.\n", status: :unauthorized
end
end
end
end
end