app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
include Mixins::BreadcrumbsMixin
include DashboardHelper
include Mixins::StartUrl
menu_section :vi
@@items_per_page = 8
before_action :check_privileges, :except => %i[csp_report authenticate
external_authenticate kerberos_authenticate
logout login login_retry wait_for_task
saml_login initiate_saml_login
oidc_login initiate_oidc_login]
before_action :get_session_data, :except => %i[csp_report authenticate
external_authenticate kerberos_authenticate saml_login oidc_login]
after_action :cleanup_action, :except => %i[csp_report]
def index
redirect_to(:action => 'show')
end
def current_hostname
return URI.parse(request.env['HTTP_X_FORWARDED_FOR']).hostname if request.env['HTTP_X_FORWARDED_FOR']
URI.parse(request.original_url).hostname
end
def saml_protected_page
request.base_url + '/saml_login'
end
helper_method :saml_protected_page
def oidc_protected_page
request.base_url + '/oidc_login'
end
helper_method(:oidc_protected_page)
def oidc_protected_page_logout
request.base_url + '/oidc_login/redirect_uri?logout=' + CGI.escape(request.base_url)
end
helper_method(:oidc_protected_page_logout)
def iframe
assert_privileges("dashboard_view")
override_content_security_policy_directives(:frame_src => ['*'])
override_x_frame_options('')
@layout = nil
if params[:id].present?
item = Menu::Manager.item(params[:id])
@layout = item.id if item.present?
end
@big_iframe = true
render :locals => {:iframe_src => item.href}
end
def csp_report
report = ActiveSupport::JSON.decode(request.body.read)
$log.warn("security warning, CSP violation report follows: #{report.inspect}")
head :ok
end
# New tab was pressed
def change_tab
assert_privileges("dashboard_view")
tab_vars(params['uib-tab'])
show
render :action => "show"
end
def start_url
redirect_to(start_url_for_user(nil))
end
def widget_chart_data
assert_privileges("dashboard_view")
widget = find_record_with_rbac(MiqWidget, params[:id])
widget_content = widget.contents_for_user(current_user)
blank = widget_content.blank?
datum = blank ? nil : widget_content.contents
content = nil
if datum.blank?
state = 'no_data'
elsif ManageIQ::Reporting::Charting.data_ok?(datum)
content = r[:partial => "widget_chart", :locals => {:widget => widget}].html_safe
state = 'valid'
else
state = 'invalid'
end
render :json => {:content => content,
:minimized => widget_minimized?(params[:id]),
:blank => blank,
:state => state}
end
def widget_menu_data
assert_privileges("dashboard_view")
widget = find_record_with_rbac(MiqWidget, params[:id])
shortcuts = widget.miq_widget_shortcuts.order("sequence").select do |shortcut|
role_allows?(:feature => shortcut.miq_shortcut.rbac_feature_name, :any => true)
end.map do |shortcut|
{:description => shortcut.description, :href => shortcut.miq_shortcut.url}
end
render :json => {:content => shortcuts,
:minimized => widget_minimized?(params[:id]),
:blank => false}
end
def widget_minimized?(id)
@sb[:dashboards][@sb[:active_db]][:minimized].include?(id)
end
private :widget_minimized?
def widget_report_data
assert_privileges("dashboard_view")
widget = find_record_with_rbac(MiqWidget, params[:id])
widget_content = widget.contents_for_user(current_user)
blank = widget_content.blank?
content = blank ? nil : update_content(widget_content.contents, :striped_hover, 'dashboard')
render :json => {
:blank => blank,
:content => content,
:minimized => widget_minimized?(params[:id])
}
end
def show
assert_privileges("dashboard_view")
@layout = "dashboard"
@display = "dashboard"
@lastaction = "show"
@title = _("Dashboard")
records = current_group.ordered_widget_sets
@tabs = []
active_tab_id = (params['uib-tab'] || @sb[:active_db_id]).try(:to_s)
active_tab = active_tab_id && records.detect { |r| r.id.to_s == active_tab_id } || records.first
# load first one on intial load, or load tab from params['uib-tab'] changed,
# or when coming back from another screen load active tab from sandbox
if active_tab
@active_tab = active_tab.id.to_s
@sb[:active_db] = active_tab.name
@sb[:active_db_id] = active_tab.id
end
tab_vars(active_tab_id)
records.each do |db|
@tabs.push([db.id.to_s, db.description])
# check this only first time when user logs in comes to dashboard show
next if @sb[:dashboards]
# get user dashboard version
ws = MiqWidgetSet.where_unique_on(db.name, current_user).first
# update user's copy if group dashboard has been updated by admin
if ws&.set_data && (db.set_data[:reset_upon_login] || (!ws.set_data[:last_group_db_updated] ||
(ws.set_data[:last_group_db_updated] && db.updated_on > ws.set_data[:last_group_db_updated])))
# if group dashboard was locked earlier but now it is unlocked,
# reset everything OR if admin makes changes to a locked db do a reset on user's copies
if db.set_data[:reset_upon_login] || (db.set_data[:locked] && !ws.set_data[:locked]) || (db.set_data[:locked] && ws.set_data[:locked])
ws.set_data = db.set_data
ws.set_data[:last_group_db_updated] = db.updated_on
ws.save
# if group dashboard was unloacked earlier but now it is locked,
# only change locked flag of users dashboard version
elsif !db.set_data[:locked] && ws.set_data[:locked]
ws.set_data[:locked] = db.set_data[:locked]
ws.set_data[:last_group_db_updated] = db.updated_on unless ws.set_data[:last_group_db_updated]
ws.save
end
end
end
@sb[:dashboards] ||= {}
ws = MiqWidgetSet.where_unique_on(@sb[:active_db], current_user).first
# if all of user groups dashboards have been deleted and they are logged in, need to reset active_db_id
if ws.nil?
@sb[:active_db_id] = nil unless MiqWidgetSet.exists?(:id => @sb[:active_db_id])
end
# Create default dashboard for this user, if not present
ws = create_user_dashboard(@sb[:active_db_id]) if ws.nil?
# Set tabs now if user's group didnt have any dashboards using default dashboard
if records.empty?
db = MiqWidgetSet.find(@sb[:active_db_id])
@active_tab = ws.id.to_s
@tabs.push([ws.id.to_s, db.description])
# User's group has dashboards, delete userid|default dashboard if it exists, dont need to keep that
else
db = MiqWidgetSet.where_unique_on("default", current_user).first
db.destroy if db.present?
end
@sb[:dashboards][@sb[:active_db]] = ws.set_data
@sb[:dashboards][@sb[:active_db]][:minimized] ||= [] # Init minimized widgets array
# Build the available widgets for the pulldown
col_widgets = column_widgets(@sb[:dashboards][@sb[:active_db]]).flatten.uniq.compact
# Build widget_list to load the widget dropdown list toolbar
widget_list = []
prev_type = nil
@available_widgets = []
MiqWidget.available_for_user(current_user).sort_by { |a| a.content_type + a.title.downcase }.each do |w|
@available_widgets.push(w.id) # Keep track of widgets available to this user
next if col_widgets.include?(w.id) || !w.enabled
image, tip = case w.content_type
when "menu" then ["fa fa-share-square-o fa-lg", _("Add this Menu Widget")]
when "chart" then ["fa fa-pie-chart fa-lg", _("Add this Chart Widget")]
when "report" then ["fa fa-file-text-o fa-lg", _("Add this Report Widget")]
end
if prev_type && prev_type != w.content_type
widget_list << {:id => w.content_type, :type => :separator}
end
prev_type = w.content_type
widget_list << {
:id => w.id,
:type => :button,
:text => _(w.title),
:image => image.to_s,
:title => tip
}
end
can_add = role_allows?(:feature => "dashboard_add")
can_reset = role_allows?(:feature => "dashboard_reset")
if can_add || can_reset
@widgets_menu = {}
if widget_list.blank?
@widgets_menu[:blank] = true
else
@widgets_menu[:allow_add] = can_add
@widgets_menu[:locked] = @sb[:dashboards][@sb[:active_db]][:locked] if can_add
@widgets_menu[:items] = widget_list
end
@widgets_menu[:allow_reset] = can_reset
end
# Make widget presenter forget chart data from previous HTTP request handled
# by this process.
WidgetPresenter.reset_data
end
# Destroy and recreate a user's dashboard from the default
def reset_widgets
assert_privileges("dashboard_reset")
ws = MiqWidgetSet.where_unique_on(@sb[:active_db], current_user).first
ws&.destroy
create_user_dashboard(@sb[:active_db_id])
@sb[:dashboards] = nil # Reset dashboards hash so it gets recreated
javascript_redirect(:action => 'show')
end
def param_widgets(column)
column.collect { |w| w.split("_").last.to_i }
end
def param_widgets(column)
column.collect { |w| w.split("_").last.to_i }
end
# A widget has been dropped
def widget_dd_done
assert_privileges("dashboard_add")
if params[:col1] || params[:col2]
if params[:col1] && params[:col1] != [""]
@sb[:dashboards][@sb[:active_db]][:col1] = param_widgets(params[:col1])
@sb[:dashboards][@sb[:active_db]][:col2].delete_if { |w| @sb[:dashboards][@sb[:active_db]][:col1].include?(w) }
elsif params[:col2] && params[:col2] != [""]
@sb[:dashboards][@sb[:active_db]][:col2] = param_widgets(params[:col2])
@sb[:dashboards][@sb[:active_db]][:col1].delete_if { |w| @sb[:dashboards][@sb[:active_db]][:col2].include?(w) }
end
save_user_dashboards
end
head :ok # We have nothing to say :)
end
# A widget has been closed
def widget_close
assert_privileges("dashboard_view")
if params[:widget] # Make sure we got a widget in
w = params[:widget].to_i
@sb[:dashboards][@sb[:active_db]][:col1].delete(w)
@sb[:dashboards][@sb[:active_db]][:col2].delete(w)
@sb[:dashboards][@sb[:active_db]][:minimized].delete(w)
ws = MiqWidgetSet.where_unique_on(@sb[:active_db], current_user).first
w = MiqWidget.find_by(:id => w)
ws.remove_member(w) if w
render :json => {:message => _("Widget \"#{w.title}\" removed")}, :status => save_user_dashboards ? 200 : 400
else
head :ok
end
end
# A widget has been added
def widget_add
assert_privileges("dashboard_add")
if params[:widget] # Make sure we got a widget in
w = params[:widget].to_i
if @sb[:dashboards][@sb[:active_db]][:col2].length < @sb[:dashboards][@sb[:active_db]][:col1].length
@sb[:dashboards][@sb[:active_db]][:col2].insert(0, w)
else
@sb[:dashboards][@sb[:active_db]][:col1].insert(0, w)
end
ws = MiqWidgetSet.where_unique_on(@sb[:active_db], current_user).first
w = MiqWidget.find(w)
if ws.add_member(w).present?
save_user_dashboards
w.create_initial_content_for_user(session[:userid])
javascript_redirect(:action => 'show')
else
render_flash(_("The widget \"%{widget_name}\" is already part of the edited dashboard") %
{:widget_name => w.name}, :error)
end
else
head :ok
end
end
# Methods to handle login/authenticate/logout functions
def login
if ext_auth?(:saml_enabled) && ext_auth?(:local_login_disabled)
redirect_to(saml_protected_page)
return
end
if ext_auth?(:oidc_enabled) && ext_auth?(:local_login_disabled)
redirect_to(oidc_protected_page)
return
end
if ::Settings.product.allow_passed_in_credentials # Only pre-populate credentials if setting is turned on
@user_name = params[:user_name]
@user_password = params[:user_password]
end
@settings = copy_hash(DEFAULT_SETTINGS) # Need settings, else pages won't display
@more = params[:type] && params[:type] != "less"
add_flash(_("Session was timed out due to inactivity. Please log in again."), :error) if params[:timeout] == "true"
logon_details = MiqServer.my_server(true).logon_status_details
@login_message = logon_details[:message] if logon_details[:status] == :starting && logon_details[:message]
if session[:user_validation_error]
add_flash(session[:user_validation_error], :error)
session[:user_validation_error] = nil
end
render :layout => "login"
end
# AJAX login retry method
def login_retry
# if MiqServer.my_server(true).logon_status == :starting
logon_details = MiqServer.my_server(true).logon_status_details
if logon_details[:status] == :starting
render :update do |page|
page << javascript_prologue
@login_message = logon_details[:message] if logon_details[:message]
page.replace("login_message_div", :partial => "login_message")
page << "setTimeout(\"#{remote_function(:url => {:action => 'login_retry'})}\", 10000);"
end
else
javascript_redirect(:action => 'login')
end
end
# Initiate a SAML Login from the main login page
def initiate_saml_login
javascript_redirect(saml_protected_page)
end
# Initiate an OpenIDC Login from the main login page
def initiate_oidc_login
javascript_redirect(oidc_protected_page)
end
# Login support for OpenIDC - GET /oidc_login
def oidc_login
identity_provider_login("oidc_login")
end
# Login support for SAML - GET /saml_login
def saml_login
identity_provider_login("saml_login")
end
# Handle external-auth signon from login screen
def external_authenticate
authenticate_external_user
end
# Handle single-signon from login screen
def kerberos_authenticate
authenticate_external_user
end
# Handle user credentials from login screen
def authenticate
@layout = "dashboard"
unless params[:task_id] # First time thru, check for buttons pressed
# Handle More and Back buttons (for changing password)
case params[:button]
when "more"
@more = true
render :update do |page|
page << javascript_prologue
page.replace("login_more_div", :partial => "login_more")
page << javascript_focus('user_new_password')
page << javascript_show("back_button")
page << javascript_hide("more_button")
end
return
when "back"
render :update do |page|
page << javascript_prologue
page.replace("login_more_div", :partial => "login_more")
page << javascript_focus('user_name')
page << javascript_hide("back_button")
page << javascript_show("more_button")
end
return
end
end
user = {
:name => params[:user_name],
:password => params[:user_password],
:new_password => params[:user_new_password],
:verify_password => params[:user_verify_password]
}
if params[:user_name].blank? && params[:user_password].blank? &&
request.headers["X-Remote-User"].blank? &&
::Settings.authentication.mode == "httpd" &&
::Settings.authentication.sso_enabled &&
params[:action] == "authenticate"
javascript_redirect(root_path)
return
end
validation = validate_user(user, params[:task_id], request)
case validation.result
when :wait_for_task
# noop, page content already set by initiate_wait_for_task
when :pass
render :update do |page|
page << javascript_prologue
page.redirect_to(validation.url)
end
when :fail
clear_current_user
add_flash(validation.flash_msg || _("Error: Authentication failed"), :error)
render :update do |page|
page << javascript_prologue
page.replace("flash_msg_div", :partial => "layouts/flash_msg")
page << "miqScrollTop();" if @flash_array.present?
page << javascript_show("flash_div")
page << "miqAjaxAuthFail();"
page << "miqSparkle(false);"
end
end
end
def logout
current_user.try(:logoff)
clear_current_user
user_validation_error = session[:user_validation_error]
session.clear
session[:auto_login] = false
session[:user_validation_error] = user_validation_error if user_validation_error
# For SAML, let's do the SAML logout to clear mod_auth_mellon IdP cookies and such
if ext_auth?(:saml_enabled)
redirect_to("/saml2/logout?ReturnTo=/")
elsif ext_auth?(:oidc_enabled)
redirect_to(oidc_protected_page_logout)
else
redirect_to(:action => 'login')
end
end
# User request to change to a different eligible group
def change_group
# Get the user and new group and set current_group in the user record
db_user = current_user
db_user.update(:current_group => db_user.miq_groups.find_by!(:id => params[:to_group]))
# Rebuild the session
session_reset
session_init(db_user)
session[:group_changed] = true
url = start_url_for_user(nil) || url_for_only_path(:controller => params[:controller], :action => 'show')
javascript_redirect(url)
end
# Put out error msg if user's role is not authorized for an action
def auth_error
add_flash(_("The user is not authorized for this task or item."), :error)
add_flash(_("Press your browser's Back button or click a tab to continue"))
end
def session_reset
# save some fields to recover back into session hash after session is cleared
keys_to_restore = %i[browser user_TZO]
data_to_restore = keys_to_restore.each_with_object({}) { |k, v| v[k] = session[k] }
session.clear
session.update(data_to_restore)
# Clear instance vars that end up in the session
@sb = @edit = @view = @settings = @lastaction = @perf_options = @assign = nil
@pp_choices = @panels = @breadcrumbs = nil
end
# Initialize session hash variables for the logged in user
def session_init(db_user)
self.current_user = db_user
# Load settings for this user, if they exist
@settings = copy_hash(DEFAULT_SETTINGS) # Start with defaults
unless db_user.nil? || db_user.settings.nil? # If the user has saved settings
db_user.settings.delete(:dashboard) # Remove pre-v4 dashboard settings
db_user.settings.delete(:db_item_min)
@settings.each { |key, value| value.merge!(db_user.settings[key]) unless db_user.settings[key].nil? }
@settings[:default_search] = db_user.settings[:default_search] # Get the user's default search setting
end
session[:user_TZO] = params[:user_TZO] ? params[:user_TZO].to_i : nil # Grab the timezone (future use)
session[:browser] ||= Hash.new("Unknown")
if params[:browser_name]
session[:browser][:name] = params[:browser_name].to_s.downcase
session[:browser][:name_ui] = params[:browser_name]
end
session[:browser][:version] = params[:browser_version] if params[:browser_version]
if params[:browser_os]
session[:browser][:os] = params[:browser_os].to_s.downcase
session[:browser][:os_ui] = params[:browser_os]
end
end
private
def authenticate_external_user
if @user_name.blank? && request.headers["X-Remote-User"].present?
user_header = request.headers["X-Remote-User"].force_encoding("UTF-8")
@user_name = params[:user_name] = user_header.split("@").first
end
authenticate
end
def validate_user(user, task_id = nil, request = nil, authenticate_options = {})
UserValidationService.new(self).validate_user(user, task_id, request, authenticate_options)
end
# Create a user's dashboard, pass in dashboard id if that is used to copy else use default dashboard
def create_user_dashboard(db_id = nil)
db = db_id ? MiqWidgetSet.find_by(:id => db_id) : MiqWidgetSet.where_unique_on("default").first
ws = MiqWidgetSet.where_unique_on(db.name, current_user).first
if ws.nil?
# Create new db if it doesn't exist
ws = MiqWidgetSet.new(:name => db.name,
:group_id => current_group_id,
:userid => current_userid,
:description => _("%{name} dashboard for user %{id} in group id %{current_group_id}") %
{:name => db.name,
:id => current_userid,
:current_group_id => current_group_id})
ws.set_data = db.set_data
ws.set_data[:last_group_db_updated] = db.updated_on
ws.save!
ws.replace_children(db.children)
ws.members.each { |w| w.create_initial_content_for_user(session[:userid]) } # Generate content if not there
end
unless db_id # set active_db and id and tabs now if user's group didnt have any dashboards
@sb[:active_db] = db.name
@sb[:active_db_id] = db.id
end
ws
end
# Save dashboards for user
def save_user_dashboards
ws = MiqWidgetSet.where_unique_on(@sb[:active_db], current_user).first
ws.set_data = @sb[:dashboards][@sb[:active_db]]
ws.save
end
def get_session_data
@layout = "login"
end
def tab_vars(current_tab)
@path = '/dashboard/change_tab/'
@current_tab = current_tab.to_s
end
def identity_provider_login(identity_type)
if @user_name.blank? && request.env["HTTP_X_REMOTE_USER"].present?
user_env = request.env["HTTP_X_REMOTE_USER"].force_encoding("UTF-8")
@user_name = params[:user_name] = user_env.split("@").first
else
redirect_to(:action => 'logout')
return
end
user = {:name => @user_name}
validation = validate_user(user, nil, request, :require_user => true, :timeout => 30)
case validation.result
when :pass
render :template => "dashboard/#{identity_type}",
:layout => false,
:locals => {:validation_url => validation.url}
nil
when :fail
session[:user_validation_error] = validation.flash_msg || "User validation failed"
redirect_to(:action => 'logout')
nil
end
end
def breadcrumbs_options
if action_name == "auth_error"
{
:breadcrumbs => [
{:title => _("Authorization Error")}
]
}
else
{
:breadcrumbs => [
{:title => _("Overview")},
{:title => (action_name == "show" ? _("Dashboard") : _("Timelines"))},
]
}
end
end
end