app/models/miq_policy.rb
# TODO: Import/Export support
class MiqPolicy < ApplicationRecord
include ReadOnlyMixin
TOWHAT_APPLIES_TO_CLASSES = %w[ContainerGroup
ContainerImage
ContainerNode
ContainerProject
ContainerReplicator
ExtManagementSystem
Host
PhysicalServer
Vm].freeze
CONDITION_SUCCESS = N_("Success")
CONDITION_FAILURE = N_("Failure")
def self.policy_towhat_applies_to_classes
TOWHAT_APPLIES_TO_CLASSES.index_with { |key| ui_lookup(:model => key) }
end
def self.policy_modes
{
"control" => _("Control"),
"compliance" => _("Compliance")
}
end
acts_as_miq_taggable
acts_as_miq_set_member
include ImportExport
include UuidMixin
include YamlImportExportMixin
before_validation :default_name_to_guid, :on => :create
default_value_for :towhat, 'Vm'
default_value_for :active, true
default_value_for :mode, 'control'
# NOTE: If another class references MiqPolicy through an ActiveRecord association,
# particularly has_one and belongs_to, calling .conditions will result in
# that method being directly called on the proxy object, as opposed to the
# target object, since that method is defined on the proxy object. The
# workaround is to call .target on the proxy first before calling .conditions.
# Additionally, this reflection could be renamed to not cause a conflict.
has_and_belongs_to_many :conditions
has_many :miq_policy_contents, :dependent => :destroy
has_many :policy_events
virtual_has_many :miq_event_definitions, :uses => {:miq_policy_contents => :miq_event_definition}
validates :description, :presence => true, :uniqueness_when_changed => true
validates :name, :presence => true, :uniqueness_when_changed => true
validates :mode, :inclusion => {:in => %w[compliance control]}
validates :towhat, :inclusion => {:in => TOWHAT_APPLIES_TO_CLASSES,
:message => "should be one of #{TOWHAT_APPLIES_TO_CLASSES.join(", ")}"}
scope :with_mode, ->(mode) { where(:mode => mode) }
scope :with_towhat, ->(towhat) { where(:towhat => towhat) }
serialize :expression
virtual_column :display_name, :type => :string
attr_accessor :reserved
class_attribute :associations_to_get_policies, :default => [:parent_enterprise, :ext_management_system, :parent_datacenter, :ems_cluster, :parent_resource_pool, :host]
@@built_in_policies = nil
def self.built_in_policies
return @@built_in_policies.dup unless @@built_in_policies.nil?
policy_hashes = YAML.load_file(Rails.root.join("product/policy/built_in_policies.yml"))
@@built_in_policies = policy_hashes.collect do |p_hash|
policy = OpenStruct.new(p_hash)
policy.attributes =
{
"name" => "(Built-in) #{p_hash[:name]}",
"description" => "(Built-in) #{p_hash[:description]}",
:applies_to? => p_hash[:applies_to?]
}
policy.events = [MiqEventDefinition.find_by(:name => policy.event)]
policy.conditions =
if policy.condition
[Condition.new(
:name => policy.attributes["name"],
:description => policy.attributes["description"],
:expression => MiqExpression.new(policy.condition),
:towhat => "Vm"
)]
else
[]
end
policy.actions_for_event = [MiqAction.find_by(:name => policy.action)]
p_metaclass = class << policy; self; end
p_metaclass.send(:define_method, :applies_to?) { |*_args| p_hash[:applies_to?] }
policy
end
@@built_in_policies.dup
end
CLEAN_ATTRS = %w[id guid name created_on updated_on miq_policy_id description]
def self.clean_attrs(attrs)
CLEAN_ATTRS.each { |a| attrs.delete(a) }
attrs
end
def display_name
"#{towhat.constantize.display_name} #{mode.capitalize}: #{description}"
end
def copy(new_fields)
npolicy = self.class.new(self.class.clean_attrs(attributes).merge(new_fields))
npolicy.conditions = conditions
npolicy.miq_policy_contents = miq_policy_contents.collect do |pc|
MiqPolicyContent.new(self.class.clean_attrs(pc.attributes))
end
npolicy.tap(&:save!)
end
def miq_event_definitions
miq_policy_contents.collect(&:miq_event_definition).compact.uniq
end
alias_method :events, :miq_event_definitions
def miq_actions
miq_policy_contents.collect(&:miq_action).compact.uniq
end
alias_method :actions, :miq_actions
def actions_for_event(event, on = :failure)
order = on == :success ? "success_sequence" : "failure_sequence"
miq_policy_contents.where(:miq_event_definition => event).order(order).collect do |pe|
next unless pe.qualifier == on.to_s
pe.get_action(on)
end.compact
end
def delete_event(event)
MiqPolicyContent.where(:miq_policy => self, :miq_event_definition => event).destroy_all
end
def add_event(event)
MiqPolicyContent.create(:miq_policy => self, :miq_event_definition => event)
end
def sync_events(events)
cevents = miq_event_definitions
adds = events - cevents
deletes = cevents - events
deletes.each { |e| delete_event(e) }
adds.each { |e| add_event(e) }
end
def replace_actions_for_event(event, action_list)
delete_event(event)
return if action_list.blank?
succes_seq = 0
fail_seq = 0
action_list.each do |action, opts|
opts[:qualifier] ||= "failure"
opts[:sequence] = opts[:qualifier].to_s == "success" ? succes_seq += 1 : fail_seq += 1
add_action_for_event(event, action, opts)
end
end
def self.enforce_policy(target, event, inputs = {})
return unless target.respond_to?(:get_policies)
result = {:result => true, :details => []}
erec = find_event_def(event)
if erec.nil?
logger.info("MIQ(policy-enforce_policy): Event: [#{event}], not defined, skipping policy enforcement")
return result
end
logger.info("MIQ(policy-enforce_policy): Event: [#{event}], To: [#{target.name}]")
mode = event.ends_with?("compliance_check") ? "compliance" : "control"
profiles, plist = get_policies_for_target(target, mode, erec, inputs)
return result if plist.blank?
succeeded, failed = evaluate_conditions(plist, target, mode, inputs, result)
# inject policy results into errors attribute of "target" object
target.errors.clear
# TODO: If we need this validation on the object, create a real/virtual attribute so ActiveModel doesn't yell
target.errors.add(:smart, result[:result])
actions = invoke_actions(target, mode, profiles, succeeded, failed, inputs.merge(:event => erec))
result[:actions] = actions if actions
result
end
def self.find_event_def(event)
# rsop event doesn't exist. It's used to run rsop without taking any actions
if event == 'rsop'
MiqEventDefinition.new(:name => event)
else
MiqEventDefinition.find_by(:name => event)
end
end
def self.display_name(number = 1)
n_('Policy', 'Policies', number)
end
private_class_method :find_event_def
def self.evaluate_conditions(plist, target, mode, inputs, result)
failed = []
succeeded = []
plist.each do |p|
logger.info("MIQ(policy-enforce_policy): Resolving policy [#{p.description}]...")
if p.conditions.empty?
always_condition = {"id" => nil, "description" => "always", "result" => "allow"}
result[:details].push(p.attributes.merge("result" => true, "conditions" => [always_condition]))
succeeded.push(p)
next
end
cond_result, clist = evaluate_conditions_for_policy(target, p, mode, inputs)
if cond_result == "deny"
result[:result] = false
result[:details].push(p.attributes.merge("result" => false, "conditions" => clist))
failed.push(p)
else
result[:details].push(p.attributes.merge("result" => true, "conditions" => clist))
succeeded.push(p)
end
end
[succeeded, failed]
end
private_class_method :evaluate_conditions
def self.evaluate_conditions_for_policy(target, policy, mode, inputs)
cond_result = "allow"
clist = []
policy.conditions.uniq.each do |c|
unless c.applies_to?(target, inputs)
# skip conditions that do not apply based on applies_to_exp
logger.info("MIQ(policy-enforce_policy): Resolving policy [#{policy.description}], Condition: [#{c.description}] does not apply, skipping...")
next
end
eval_result = eval_condition(c, target, inputs)
cond_result = eval_result if eval_result == "deny"
clist.push(c.attributes.merge("result" => eval_result))
break if eval_result == "deny" && mode == "control"
end
[cond_result, clist]
end
private_class_method :evaluate_conditions_for_policy
def self.invoke_actions(target, mode, profiles, succeeded, failed, inputs)
# don't create policy events or invoke actions if we're doing rsop
event = inputs[:event]
return if event.name == 'rsop'
if mode == 'control'
pevent = build_results(failed, profiles, event, :failure) + build_results(succeeded, profiles, event, :success)
PolicyEvent.create_events(target, event, pevent)
end
MiqAction.invoke_actions(target, inputs, succeeded, failed)
end
private_class_method :invoke_actions
def self.build_results(policies, profiles, event, status)
# [
# :miq_policy => MiqPolicy#Object
# :result => ...,
# :miq_actions => [...],
# :miq_policy_sets => [...]
# ]
policies.collect do |p|
next unless p.kind_of?(self) # skip built-in policies
{
:miq_policy => p,
:result => status.to_s,
:miq_actions => p.actions_for_event(event, status).uniq,
:miq_policy_sets => p.memberof.select { |ps| profiles.include?(ps) }
}
end.compact
end
private_class_method :build_results
def self.resolve(rec, list = nil, event = nil)
# list is expected to be a list of policies, not profiles.
policies = list.nil? ? all : where(:name => list)
policies.collect do |p|
next if event && !p.events.include?(event)
policy_hash = {"result" => "N/A", "conditions" => [], "actions" => []}
policy_hash["scope"] = MiqExpression.evaluate_atoms(p.expression, rec) unless p.expression.nil?
if policy_hash["scope"].nil? || policy_hash["scope"]["result"]
policy_hash['result'], policy_hash['conditions'] = resolve_policy_conditions(p, rec)
action_on = policy_hash["result"] == "deny" ? :failure : :success
policy_hash["actions"] =
p.actions_for_event(event, action_on).uniq.collect do |a|
{"id" => a.id, "name" => a.name, "description" => a.description, "result" => policy_hash["result"]}
end unless event.nil?
end
p.attributes.merge(policy_hash)
end.compact
end
def self.resolve_policy_conditions(policy, rec)
policy_result = 'allow'
conditions =
policy.conditions.collect do |c|
rec_model = rec.class.base_model.name
rec_model = "Vm" if rec_model.downcase.match?("template")
next unless rec_model == c["towhat"]
resolve_condition(c, rec).tap do |cond_hash|
policy_result = cond_hash["result"] if cond_hash["result"] == "deny"
end
end.compact
if policy.active == true
result_list = conditions.collect { |c| c["result"] }.uniq
policy_result = result_list.first if result_list.length == 1 && result_list.first == "N/A"
else
policy_result = "N/A" # Ignore condition result if policy is inactive
end
[policy_result, conditions]
end
private_class_method :resolve_policy_conditions
def self.resolve_condition(cond, rec)
cond_hash = {"id" => cond.id, "name" => cond.name, "description" => cond.description}
cond_hash["scope"] = MiqExpression.evaluate_atoms(cond.applies_to_exp, rec) unless cond.applies_to_exp.nil?
if cond_hash["scope"].nil? || cond_hash["scope"]["result"]
cond_hash["result"] = eval_condition(cond, rec)
cond_hash["expression"] = MiqExpression.evaluate_atoms(cond.expression, rec)
else
cond_hash["result"] = "N/A"
cond_hash["expression"] = cond.expression.exp
end
cond_hash
end
private_class_method :resolve_condition
def applies_to?(rec, inputs = {})
rec_model = rec.class.base_model.name
rec_model = "Vm" if rec_model.downcase.match?("template")
return false if towhat && rec_model != towhat
return true if expression.nil?
Condition.evaluate(self, rec, inputs)
end
def self.eval_condition(c, rec, inputs = {})
Condition.evaluate(c, rec, inputs) ? 'allow' : 'deny'
rescue => err
logger.log_backtrace(err)
end
private_class_method :eval_condition
EVENT_GROUPS_EXCLUDED = ["evm_operations", "ems_operations"]
def self.all_policy_events
MiqEventDefinition.all_events.select { |e| !e.memberof.empty? && !EVENT_GROUPS_EXCLUDED.include?(e.memberof.first.name) }
end
def self.all_policy_events_filter
# Todo Convert to SQL if possible
filter_hash = {
"AND" => [
{"=" => {"field" => "MiqEventDefinition-event_type", "value" => "Default"}},
{"!=" => {"field" => "MiqEventDefinition-event_group_name", "value" => ""}}
]
}
EVENT_GROUPS_EXCLUDED.each do |e|
filter_hash["AND"] << {"!=" => {"field" => "MiqEventDefinition-event_group_name", "value" => e}}
end
MiqExpression.new(filter_hash)
end
def self.logger
$policy_log
end
def last_event
policy_events.last.try(:created_on)
end
def first_event
policy_events.first.try(:created_on)
end
def first_and_last_event
[first_event, last_event].compact
end
def self.get_policies_for_target(target, mode, event, inputs = {})
event = find_event_def(event) if event.kind_of?(String)
# collect policies expand profiles (sets)
profiles, plist = get_expanded_profiles_and_policies(target)
plist = built_in_policies.concat(plist).uniq
towhat = target.class.base_model.name
towhat = "Vm" if towhat.downcase.match?("template")
plist.keep_if do |p|
p.mode == mode &&
p.towhat == towhat &&
policy_for_event?(p, event) &&
policy_active?(p) &&
policy_applicable?(p, target, inputs)
end
[profiles, plist]
end
def self.policy_for_event?(policy, event)
event.name == 'rsop' || policy.events.include?(event)
end
private_class_method :policy_for_event?
def self.policy_active?(policy)
return true if policy.active
logger.info("MIQ(policy-enforce_policy): Policy [#{policy.description}] is not active, skipping...")
false
end
private_class_method :policy_active?
def self.policy_applicable?(policy, target, inputs)
return true if policy.applies_to?(target, inputs)
logger.info("MIQ(policy-enforce_policy): Policy [#{policy.description}] does not apply, skipping...")
false
end
private_class_method :policy_applicable?
def self.get_expanded_profiles_and_policies(target)
# get profiles and policies from target object
target_policies = target.get_policies
target_profiles = separate_profiles_from_policies(target_policies)
# get profiles and policies from associations
assoc_policies =
associations_to_get_policies.collect do |assoc|
next unless target.respond_to?(assoc)
obj = target.send(assoc)
next unless obj
obj.get_policies
end.compact.flatten
assoc_profiles = separate_profiles_from_policies(assoc_policies)
[target_profiles.concat(assoc_profiles), target_policies.concat(assoc_policies).flatten.compact.uniq]
end
private_class_method :get_expanded_profiles_and_policies
def self.separate_profiles_from_policies(policies)
profiles = []
policies.collect! do |p|
if p.kind_of?(MiqPolicySet)
profiles.push(p)
p = p.members
end
p
end
profiles
end
private_class_method :separate_profiles_from_policies
def add_action_for_event(event, action, opt_hash = nil)
# we now expect an options hash provided by the UI, merge the qualifier with the options_hash
# overwriting with the values from the options hash
opt_hash = {:qualifier => :failure}.merge(opt_hash)
# update the correct DB sequence and synchronous value with the value from the UI
opt_hash[:qualifier] = opt_hash[:qualifier].to_s
case opt_hash[:qualifier]
when "success"
opt_hash[:success_sequence] = opt_hash[:sequence]
opt_hash[:success_synchronous] = opt_hash[:synchronous]
when "failure"
opt_hash[:failure_sequence] = opt_hash[:sequence]
opt_hash[:failure_synchronous] = opt_hash[:synchronous]
when "both"
opt_hash[:success_sequence] = opt_hash[:sequence]
opt_hash[:failure_sequence] = opt_hash[:sequence]
opt_hash[:success_synchronous] = opt_hash[:synchronous]
opt_hash[:failure_synchronous] = opt_hash[:synchronous]
end
opt_hash.delete(:sequence)
opt_hash.delete(:synchronous)
pevent = miq_policy_contents.build(opt_hash)
pevent.miq_event_definition = event
pevent.miq_action = action
pevent.save
save!
end
private :add_action_for_event
end