# 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 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? && 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 def process_quick_create_parameters return unless @rule # 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? && 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 # 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 end