app/helpers/audits_helper.rb
module AuditsHelper
AUDIT_ADD = 'add'
AUDIT_REMOVE = 'remove'
# lookup the Model representing the numerical id and return its label
def id_to_label(name, change, audit: @audit, truncate: true)
return _("N/A") if change.nil?
case name
when "ancestry"
label = change.blank? ? "" : change.split('/').map { |i| Hostgroup.find(i).name rescue _("N/A") }.join('/')
when 'last_login_on'
label = change.to_s(:short)
when /.*_id$/
begin
label = find_associated_records_using_key(name, change, audit)&.to_label
rescue NameError
# fallback to the value only instead of N/A that is in generic rescue below
return _("Missing(ID: %s)") % change
end
when /.*_ids$/
existing = find_associated_records_using_key(name, change, audit)
label = change.map do |id|
if existing&.has_key?(id)
existing[id].to_label
else
_("Missing(ID: %s)") % id
end
end.join(', ')
else
label = (change.to_s == AuditExtensions::REDACTED) ? _(change.to_s) : change.to_s
end
label = _('[empty]') unless label.present?
if truncate
label = label.truncate(50)
else
label = label.strip.split("\n")[0]
end
label
rescue
_("N/A")
end
def audit_title(audit)
type_name = audited_type audit
case type_name
when 'Puppet Class'
(id_to_label audit.audited_changes.keys[0], audit.audited_changes.values[0], audit: audit).to_s
else
name = if audit.auditable_name.blank?
revision = audit.revision
(revision.respond_to?(:to_audit_label) && revision.to_audit_label) || revision.to_label
else
audit.auditable_name
end
name += " / #{audit.associated_name}" if audit.associated_id && audit.associated_name.present? && type_name != 'Interface'
name
end
rescue StandardError => exception
Foreman::Logging.exception("Could not render audit_title", exception)
""
end
def details(audit, path = audits_path(:search => "id=#{audit.id}"))
if audit.action == 'update'
return [] unless audit.audited_changes.present?
audit.audited_changes.map do |name, change|
next if change.nil? || change.to_s.empty?
case name
when 'template'
(_("Template content changed %s") % (link_to 'view diff', path)).html_safe if audit_template? audit
when "password_changed"
_("Password has been changed")
when "owner_id", "owner_type"
_("Owner changed to %s") % (audit.revision.owner rescue _('N/A'))
when 'global_status'
base = audit.audited_changes.values[0]
from = HostStatus::Global.new(base[0]).to_label
to = HostStatus::Global.new(base[1]).to_label
_("Global status changed from %{from} to %{to}") % { :from => from, :to => to }
else
_("%{name} changed from %{label1} to %{label2}") % {
:name => name.humanize,
:label1 => id_to_label(name, change[0], audit: audit),
:label2 => id_to_label(name, change[1], audit: audit) }
end
end
elsif !main_object? audit
from = id_to_label(audit.audited_changes.keys[0], audit.audited_changes.values[0], audit: audit)
to = audit.associated_name || id_to_label(audit.audited_changes.keys[1], audit.audited_changes.values[1], audit: audit)
case audit_action_name(audit)
when AUDIT_ADD
[_("Added %{from} to %{to}") % {:from => from, :to => to}]
when AUDIT_REMOVE
[_("Removed %{from} from %{to}") % {:from => from, :to => to}]
end
else
[]
end
end
def audit_template?(audit)
return false if audit.audited_changes.blank?
audit.audited_changes["template"] && audit.audited_changes["template"][0] != audit.audited_changes["template"][1]
end
def audit_login?(audit)
name = audit.audited_changes.keys[0] rescue ''
name == 'last_login_on'
end
def audit_action_name(audit)
return audit.action if main_object? audit
case audit.action
when 'create'
AUDIT_ADD
when 'destroy'
AUDIT_REMOVE
else
'update'
end
end
def audit_user(audit)
return if audit.username.nil?
login = audit.user_login
link_to(icon_text('user', audit.username.gsub(_('User'), '')), hash_for_audits_path(:search => login ? "user = #{login}" : "username = \"#{audit.username}\""))
end
def audit_time(audit)
date_time_absolute(audit.created_at)
end
def audited_icon(audit)
style = 'label-info'
if main_object? audit
style = case audit.action
when 'create'
'label-success'
when 'update'
'label-info'
when 'destroy'
'label-danger'
else
''
end
end
style += " label"
type = audited_type(audit)
symbol = case type
when "Host"
{:icon => 'server', :kind => 'pficon'}
when "Hostgroup"
{:icon => 'server-group', :kind => 'pficon'}
when 'Interface'
{:icon => 'network', :kind => 'pficon'}
when "User"
{:icon => 'user', :kind => 'fa'}
else
{:icon => 'cog', :kind => 'fa'}
end
content_tag(:b, icon_text(symbol[:icon], type, :class => 'icon-white', :kind => symbol[:kind]), :class => style)
end
def audited_type(audit)
type_name = case audit.auditable_type
when 'Host::Base'
'Host'
when 'HostClass'
'Puppet Class'
when 'Parameter'
"#{audit.associated_type || 'Global'}-#{type_name}"
when 'PuppetclassLookupKey'
'Smart Class Parameter'
when 'LookupValue'
'Override Value'
when 'Ptable'
'Partition Table'
when /^Nic/
'Interface'
else
audit.auditable_type
end
type_name.underscore.titleize
end
def audit_remote_address(audit)
return if audit.remote_address.empty?
content_tag :p, :style => 'color:#999;' do
"(" + audit.remote_address + ")"
end
end
def nested_host_audit_breadcrumbs
return unless @host
breadcrumbs(
switchable: false,
items: [
{
caption: _("Hosts"),
url: (current_hosts_path if authorized_for(hash_for_hosts_path)),
},
{
caption: @host.name,
url: (current_host_details_path(@host) if authorized_for(hash_for_host_path(@host))),
},
{
caption: _('Audits'),
url: audits_path,
},
]
)
end
def construct_additional_info(audits)
audits.map do |audit|
action_display_name = audit_action_name(audit)
audit.attributes.merge!(
'action_display_name' => action_display_name,
'audited_type_name' => audited_type(audit),
'user_info' => user_info(audit),
'audit_title' => audit_title(audit),
'audit_title_url' => audit_title_url(audit),
'affected_locations' => fetch_affected_locations(audit),
'affected_organizations' => fetch_affected_organizations(audit),
'details' => additional_details_if_any(audit, action_display_name),
'audited_changes_with_id_to_label' => audit.audited_changes.blank? ? [] : rebuild_audit_changes(audit),
'allowed_actions' => actions_allowed(audit)
)
end
end
private
def additional_details_if_any(audit, action_display_name)
[AUDIT_ADD, AUDIT_REMOVE].include?(action_display_name) ? details(audit) : []
end
def main_object?(audit)
main_objects_names = Audit.main_object_names
return true if main_objects_names.include?(audit.auditable_type)
type = audit.auditable_type.split("::").last rescue ''
main_objects_names.include?(type)
end
def find_auditable_type_class(audit)
auditable_type = (audit.auditable_type == 'Host::Base') ? 'Host::Managed' : audit.auditable_type
auditable_type.constantize
end
def key_to_association_class(key, auditable_class)
association_name = key.gsub(/_id(s?)$/, '')
association_name = association_name.pluralize if key =~ /_ids$/
reflection_obj = auditable_class.reflect_on_association(association_name)
reflection_obj ? reflection_obj&.klass : nil
end
def find_owner_class(key, change, audit)
type = nil
if audit.audited_changes.has_key?('owner_type')
type = audit.audited_changes['owner_type']
if audit.action == 'update'
idx = audit.audited_changes['owner_id'].index(change)
type = audit.audited_changes['owner_type'][idx]
end
else
previous = Audit.where(auditable_type: audit.auditable_type, auditable_id: audit.auditable_id)
.where("id < ?", audit.id)
.where("audited_changes LIKE '%owner_type:%'")
.first
type = previous.audited_changes['owner_type']
type = type.last if previous.action == 'update'
end
type.constantize
end
def find_associated_records_using_key(key, change, audit)
auditable_class = find_auditable_type_class(audit)
association_class = if key == 'owner_id'
find_owner_class(key, change, audit)
else
key_to_association_class(key, auditable_class)
end
if association_class
case key
when /_ids$/
association_class&.where(id: change)&.index_by(&:id)
when /_id$/
association_class&.find(change)
end
elsif auditable_class.respond_to?('audit_hook_to_find_records')
auditable_class.audit_hook_to_find_records(key, change, audit)
end
end
def rebuild_audit_changes(audit)
css_class_name = css_class_by_action(audit.action == 'destroy')
# update data for created template for better view
if audit.action == 'create' && (change = audit.audited_changes['template']).present?
audit.audited_changes['template'] = ['', change]
end
audit.audited_changes.map do |name, change|
next if change.nil? || change.to_s.empty?
next if name == 'template'
rec = { :name => name.humanize }
if audit.action == 'update'
rec[:change] = change.map.with_index do |v, i|
change_info_hash(name, v, css_class_by_action(i == 0), audit: audit)
end
else
rec[:change] = (rec[:change] || []).push(change_info_hash(name, change, css_class_name, audit: audit))
end
rec
end.compact
end
def css_class_by_action(is_condition_match)
is_condition_match ? 'show-old' : 'show-new'
end
def change_info_hash(name, change, css_class = 'show-new', audit: nil)
{ :css_class => css_class, :id_to_label => id_to_label(name, change, truncate: false, audit: audit) }
end
def fetch_affected_locations(audit)
base = (audit.locations.authorized(:view_locations) + (audit.locations & User.current.my_locations)).uniq
return [] if base.empty?
authorizer = Authorizer.new(User.current, base)
base.map do |location|
options = hash_for_edit_location_path(location).merge(:auth_object => location, :permission => 'edit_locations', :authorizer => authorizer)
construct_options(location.name, edit_location_path(location), options)
end
end
def fetch_affected_organizations(audit)
base = (audit.organizations.authorized(:view_organizations) + (audit.organizations & User.current.my_organizations)).uniq
return [] if base.empty?
authorizer = Authorizer.new(User.current, base)
base.map do |organization|
options = hash_for_edit_organization_path(organization).merge(:auth_object => organization, :permission => 'edit_organizations', :authorizer => authorizer)
construct_options(organization.name, edit_organization_path(organization), options)
end
end
def construct_options(affected_obj_name, affected_obj_url, options = {})
if authorized_for(options)
{'name' => affected_obj_name, 'url' => affected_obj_url}
else
{'name' => affected_obj_name, 'url' => '#', 'css_class' => "disabled", 'disabled' => true}
end
end
def audit_title_url(audit)
keytype_array = Audit.find_complete_keytype_array(audit.auditable_type)
filter = "type = #{keytype_array.first} and auditable_id = #{audit.auditable_id}" if keytype_array.present?
(filter ? audits_path(:search => filter) : nil)
end
def user_info(audit)
return {} if audit.username.nil?
login = audit.user_login
{
'display_name' => audit.username.gsub(_('User'), ''),
'login' => login,
'search_path' => audits_path(:search => login ? "user = #{login}" : "username = \"#{audit.username}\""),
'audit_path' => audits_path(:search => "id=#{audit.id}"),
}
end
def actions_allowed(audit)
actions = []
if audit.auditable_type == 'Host::Base' && audit.auditable
actions.push(host_details_action(audit.auditable))
end
if audit.auditable_type.match(/^Nic/) && audit.associated_type == 'Host::Base' && audit.associated
actions.push(host_details_action(audit.associated, :is_associated => true))
end
actions
end
def host_details_action(host, options = {})
host_path_name = find_host_path_name(host)
action_details = { :title => _("Host details"), :css_class => 'btn btn-default' }
action_details[:name] = _("Associated Host") if options[:is_associated]
auth_options = send("hash_for_#{host_path_name}", :id => host.to_param).merge(
:auth_object => host, :auth_action => 'view')
if authorized_for(auth_options)
action_details[:url] = current_host_details_path(host)
else
action_details.merge!(:url => '#', :css_class => 'btn btn-default disabled', :disabled => true)
end
action_details
end
def find_host_path_name(host)
host_type = host.type
default_path = 'host_path'
return default_path if ['Host::Base', 'Host::Managed'].include?(host_type)
host_path_name = host_type.split('::').last.downcase + "_#{default_path}"
respond_to?(host_path_name) ? host_path_name : default_path
end
end