Files
baffle-hub/app/controllers/rules_controller.rb
Dan Milne 90823a1389 Yeh
2025-11-15 10:51:58 +11:00

357 lines
9.7 KiB
Ruby

# frozen_string_literal: true
class RulesController < ApplicationController
# Follow proper before_action order:
# 1. Authentication/Authorization
# All actions require authentication
# 2. Resource loading
before_action :set_rule, only: [:show, :edit, :update, :disable, :enable]
# GET /rules
def index
@pagy, @rules = pagy(policy_scope(Rule).includes(:user, :network_range).order(created_at: :desc))
@waf_rule_types = Rule.waf_rule_types
@waf_actions = Rule.waf_actions
end
# GET /rules/new
def new
authorize Rule
@rule = Rule.new
# Pre-fill from URL parameters
if params[:network_range_id].present?
network_range = NetworkRange.find_by(id: params[:network_range_id])
@rule.network_range = network_range if network_range
end
if params[:cidr].present?
@rule.waf_rule_type = 'network'
end
@waf_rule_types = Rule.waf_rule_types
@waf_actions = Rule.waf_actions
end
# POST /rules
def create
authorize Rule
@rule = Rule.new(rule_params)
@rule.user = Current.user
@waf_rule_types = Rule.waf_rule_types
@waf_actions = Rule.waf_actions
# Process additional form data for quick create
process_quick_create_parameters
# Handle network range creation if CIDR is provided
if params[:cidr].present? && @rule.network_rule?
network_range = NetworkRange.find_or_create_by(cidr: params[:cidr]) do |range|
range.user = Current.user
range.source = 'manual'
range.creation_reason = "Created for rule ##{@rule.id}"
end
@rule.network_range = network_range
end
# Calculate priority automatically based on rule type
calculate_rule_priority
if @rule.save
# For quick create from NetworkRange page, redirect back to network range
if params[:rule][:network_range_id].present? && request.referer&.include?('/network_ranges/')
network_range = NetworkRange.find(params[:rule][:network_range_id])
redirect_to network_range, notice: 'Rule was successfully created.'
else
redirect_to @rule, notice: 'Rule was successfully created.'
end
else
render :new, status: :unprocessable_entity
end
end
# GET /rules/:id
def show
authorize @rule
end
# GET /rules/:id/edit
def edit
authorize @rule
@waf_rule_types = Rule.waf_rule_types
@waf_actions = Rule.waf_actions
end
# PATCH/PUT /rules/:id
def update
authorize @rule
# Preserve original attributes in case validation fails
original_attributes = @rule.attributes.dup
original_network_range_id = @rule.network_range_id
if @rule.update(rule_params)
redirect_to @rule, notice: 'Rule was successfully updated.'
else
# Restore original attributes to preserve form state
# This prevents network range dropdown from resetting
@rule.attributes = original_attributes
@rule.network_range_id = original_network_range_id
render :edit, status: :unprocessable_entity
end
end
# POST /rules/:id/disable
def disable
authorize @rule, :disable?
reason = params[:reason] || "Disabled manually"
@rule.disable!(reason: reason)
redirect_to @rule, notice: 'Rule was successfully disabled.'
end
# POST /rules/:id/enable
def enable
authorize @rule, :enable?
@rule.enable!
redirect_to @rule, notice: 'Rule was successfully enabled.'
end
private
def set_rule
@rule = Rule.find(params[:id])
end
def rule_params
permitted = [
:waf_rule_type,
:waf_action,
:metadata,
:expires_at,
:enabled,
:source,
:network_range_id
]
# Only include conditions for non-network rules
if params[:rule][:waf_rule_type] != 'network'
permitted << :conditions
end
params.require(:rule).permit(permitted)
end
def calculate_rule_priority
return unless @rule
case @rule.waf_rule_type
when 'network'
# For network rules, priority based on prefix specificity
if @rule.network_range
prefix = @rule.network_range.prefix_length
@rule.priority = case prefix
when 32 then 200 # /32 single IP
when 31 then 190
when 30 then 180
when 29 then 170
when 28 then 160
when 27 then 150
when 26 then 140
when 25 then 130
when 24 then 120
when 23 then 110
when 22 then 100
when 21 then 90
when 20 then 80
when 19 then 70
when 18 then 60
when 17 then 50
when 16 then 40
when 15 then 30
when 14 then 20
when 13 then 10
else 0
end
else
@rule.priority = 100 # Default for network rules without range
end
when 'path_pattern'
@rule.priority = 85
when 'rate_limit'
@rule.priority = 70
else
@rule.priority = 50 # Default priority
end
end
def process_quick_create_parameters
return unless @rule
# Handle path pattern parameters
if @rule.path_pattern_rule? && params[:path_pattern].present? && params[:match_type].present?
begin
pattern = params[:path_pattern]
match_type = params[:match_type]
# Parse pattern to segments
segments = pattern.split('/').reject(&:blank?).map(&:downcase)
# Find or create PathSegment entries
segment_ids = segments.map do |seg|
PathSegment.find_or_create_segment(seg).id
end
# Set conditions with segment IDs and match type
@rule.conditions = {
segment_ids: segment_ids,
match_type: match_type,
original_pattern: pattern
}
# Add to metadata for display
@rule.metadata ||= {}
@rule.metadata.merge!({
segments: segments,
pattern_display: "/" + segments.join("/")
})
rescue => e
@rule.errors.add(:base, "Failed to create path pattern: #{e.message}")
end
end
# Handle rate limiting parameters
if @rule.rate_limit_rule? && params[:rate_limit].present? && params[:rate_window].present?
rate_limit_data = {
limit: params[:rate_limit].to_i,
window_seconds: params[:rate_window].to_i,
scope: 'per_ip'
}
# Update conditions with rate limit data
@rule.conditions ||= {}
@rule.conditions.merge!(rate_limit_data)
end
# Handle redirect URL
if @rule.redirect_action? && params[:redirect_url].present?
@rule.metadata ||= {}
if @rule.metadata.is_a?(String)
begin
@rule.metadata = JSON.parse(@rule.metadata)
rescue JSON::ParserError
@rule.metadata = {}
end
end
@rule.metadata.merge!({
redirect_url: params[:redirect_url],
redirect_status: 302
})
end
# Parse metadata if it's a string that looks like JSON
if @rule.metadata.is_a?(String) && @rule.metadata.starts_with?('{')
begin
@rule.metadata = JSON.parse(@rule.metadata)
rescue JSON::ParserError
# Keep as string if not valid JSON
end
end
# Handle expires_at parsing for text input
if params.dig(:rule, :expires_at).present?
expires_at_str = params[:rule][:expires_at].strip
if expires_at_str.present?
begin
# Try to parse various datetime formats
@rule.expires_at = DateTime.parse(expires_at_str)
rescue ArgumentError
# Try specific format
begin
@rule.expires_at = DateTime.strptime(expires_at_str, '%Y-%m-%d %H:%M')
rescue ArgumentError
@rule.errors.add(:expires_at, 'must be in format YYYY-MM-DD HH:MM')
end
end
end
end
# Add reason to metadata if provided
if params.dig(:rule, :metadata).present?
if @rule.metadata.is_a?(Hash)
@rule.metadata['reason'] = params[:rule][:metadata]
else
@rule.metadata = { 'reason' => params[:rule][:metadata] }
end
end
end
private
def set_rule
@rule = Rule.find(params[:id])
end
def rule_params
permitted = [
:waf_rule_type,
:waf_action,
:metadata,
:expires_at,
:enabled,
:source,
:network_range_id
]
# Only include conditions for non-network rules
if params[:rule][:waf_rule_type] != 'network'
permitted << :conditions
end
params.require(:rule).permit(permitted)
end
def calculate_rule_priority
return unless @rule
case @rule.waf_rule_type
when 'network'
# For network rules, priority based on prefix specificity
if @rule.network_range
prefix = @rule.network_range.prefix_length
@rule.priority = case prefix
when 32 then 200 # /32 single IP
when 31 then 190
when 30 then 180
when 29 then 170
when 28 then 160
when 27 then 150
when 26 then 140
when 25 then 130
when 24 then 120
when 23 then 110
when 22 then 100
when 21 then 90
when 20 then 80
when 19 then 70
when 18 then 60
when 17 then 50
when 16 then 40
when 15 then 30
when 14 then 20
when 13 then 10
else 0
end
else
@rule.priority = 100 # Default for network rules without range
end
when 'path_pattern'
@rule.priority = 85
when 'rate_limit'
@rule.priority = 70
else
@rule.priority = 50 # Default priority
end
end
end