app/messages/validators/security_group_rule_validator.rb
require 'active_model'
class RulesValidator < ActiveModel::Validator
ALLOWED_RULE_KEYS = %i[
code
description
destination
log
ports
protocol
type
].freeze
MAX_DESTINATIONS_PER_RULE = 6000
def validate(record)
unless record.rules.is_a?(Array)
record.errors.add :rules, 'must be an array'
return
end
record.rules.each_with_index do |rule, index|
unless rule.is_a?(Hash)
add_rule_error('must be an object', record, index)
next
end
validate_allowed_keys(rule, record, index)
add_rule_error("protocol must be 'tcp', 'udp', 'icmp', or 'all'", record, index) unless valid_protocol(rule[:protocol])
if valid_destination_type(rule[:destination], record, index)
rules = rule[:destination].split(',', -1)
add_rule_error("maximum destinations per rule exceeded - must be under #{MAX_DESTINATIONS_PER_RULE}", record, index) unless rules.length <= MAX_DESTINATIONS_PER_RULE
rules.each do |d|
validate_destination(d, record, index)
end
end
validate_description(rule, record, index)
validate_log(rule, record, index)
case rule[:protocol]
when 'tcp', 'udp'
validate_tcp_udp_protocol(rule, record, index)
when 'icmp'
validate_icmp_protocol(rule, record, index)
when 'all'
add_rule_error('ports are not allowed for protocols of type all', record, index) if rule[:ports]
end
end
end
def boolean?(value)
[true, false].include? value
end
def valid_protocol(protocol)
protocol.is_a?(String) && %w[tcp udp icmp all].include?(protocol)
end
def validate_allowed_keys(rule, record, index)
invalid_keys = rule.keys - ALLOWED_RULE_KEYS
add_rule_error("unknown field(s): #{invalid_keys.map(&:to_s)}", record, index) if invalid_keys.any?
end
def validate_description(rule, record, index)
add_rule_error('description must be a string', record, index) if rule[:description] && !rule[:description].is_a?(String)
end
def validate_log(rule, record, index)
add_rule_error('log must be a boolean', record, index) if rule[:log] && !boolean?(rule[:log])
end
def validate_tcp_udp_protocol(rule, record, index)
add_rule_error('ports are required for protocols of type TCP and UDP', record, index) unless rule[:ports]
return if valid_ports(rule[:ports])
add_rule_error('ports must be a valid single port, comma separated list of ports, or range or ports, formatted as a string', record, index)
end
def validate_icmp_protocol(rule, record, index)
add_rule_error('code is required for protocols of type ICMP', record, index) unless rule[:code]
add_rule_error('code must be an integer between -1 and 255 (inclusive)', record, index) unless valid_icmp_format(rule[:code])
add_rule_error('type is required for protocols of type ICMP', record, index) unless rule[:type]
add_rule_error('type must be an integer between -1 and 255 (inclusive)', record, index) unless valid_icmp_format(rule[:type])
end
def valid_icmp_format(field)
CloudController::ICMPRuleValidator.validate_icmp_control_message(field)
end
def valid_ports(ports)
return false unless ports.is_a?(String)
CloudController::TransportRuleValidator.validate_port(ports)
end
def valid_destination_type(destination, record, index)
error_message = 'destination must be a valid CIDR, IP address, or IP address range'
if CloudController::RuleValidator.comma_delimited_destinations_enabled?
error_message = 'nil destination; destination must be a comma-delimited list of valid CIDRs, IP addresses, or IP address ranges'
end
if destination.nil?
add_rule_error(error_message, record, index)
return false
end
unless destination.is_a?(String)
add_rule_error('destination must be a string', record, index)
return false
end
if /\s/ =~ destination
add_rule_error('destination must not contain whitespace', record, index)
return false
end
if !CloudController::RuleValidator.comma_delimited_destinations_enabled? && !destination.index(',').nil?
add_rule_error(error_message, record, index)
return false
end
true
end
def validate_destination(destination, record, index)
error_message = 'destination must be a valid CIDR, IP address, or IP address range'
error_message = 'destination must contain valid CIDR(s), IP address(es), or IP address range(s)' if CloudController::RuleValidator.comma_delimited_destinations_enabled?
add_rule_error('empty destination specified in comma-delimited list', record, index) if destination.empty?
address_list = destination.split('-')
if address_list.length == 1
add_rule_error(error_message, record, index) unless CloudController::RuleValidator.parse_ip(address_list.first)
elsif address_list.length == 2
ipv4s = CloudController::RuleValidator.parse_ip(address_list)
return add_rule_error('destination IP address range is invalid', record, index) unless ipv4s
sorted_ipv4s = NetAddr.sort_IPv4(ipv4s)
reversed_range_error = 'beginning of IP address range is numerically greater than the end of its range (range endpoints are inverted)'
add_rule_error(reversed_range_error, record, index) unless ipv4s.first == sorted_ipv4s.first
else
add_rule_error(error_message, record, index)
end
end
def add_rule_error(message, record, index)
record.errors.add("Rules[#{index}]:", message)
end
end