pg_sql_triggers 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/.erb_lint.yml +0 -0
  3. data/.rspec +0 -0
  4. data/.rubocop.yml +6 -16
  5. data/AGENTS.md +8 -0
  6. data/CHANGELOG.md +354 -0
  7. data/COVERAGE.md +39 -41
  8. data/LICENSE +0 -0
  9. data/README.md +44 -26
  10. data/RELEASE.md +0 -0
  11. data/Rakefile +5 -0
  12. data/app/assets/javascripts/pg_sql_triggers/application.js +0 -0
  13. data/app/assets/javascripts/pg_sql_triggers/trigger_actions.js +0 -0
  14. data/app/assets/stylesheets/pg_sql_triggers/application.css +0 -0
  15. data/app/controllers/concerns/pg_sql_triggers/error_handling.rb +0 -0
  16. data/app/controllers/concerns/pg_sql_triggers/kill_switch_protection.rb +0 -0
  17. data/app/controllers/concerns/pg_sql_triggers/permission_checking.rb +6 -5
  18. data/app/controllers/pg_sql_triggers/application_controller.rb +0 -0
  19. data/app/controllers/pg_sql_triggers/audit_logs_controller.rb +81 -64
  20. data/app/controllers/pg_sql_triggers/dashboard_controller.rb +111 -34
  21. data/app/controllers/pg_sql_triggers/migrations_controller.rb +13 -14
  22. data/app/controllers/pg_sql_triggers/tables_controller.rb +8 -0
  23. data/app/controllers/pg_sql_triggers/triggers_controller.rb +1 -0
  24. data/app/helpers/pg_sql_triggers/dashboard_helper.rb +19 -0
  25. data/app/helpers/pg_sql_triggers/permissions_helper.rb +3 -2
  26. data/app/models/pg_sql_triggers/application_record.rb +0 -0
  27. data/app/models/pg_sql_triggers/audit_log.rb +29 -47
  28. data/app/models/pg_sql_triggers/trigger_registry.rb +137 -74
  29. data/app/views/layouts/pg_sql_triggers/application.html.erb +0 -1
  30. data/app/views/pg_sql_triggers/audit_logs/index.html.erb +9 -5
  31. data/app/views/pg_sql_triggers/dashboard/index.html.erb +107 -27
  32. data/app/views/pg_sql_triggers/shared/_confirmation_modal.html.erb +0 -0
  33. data/app/views/pg_sql_triggers/shared/_kill_switch_status.html.erb +0 -0
  34. data/app/views/pg_sql_triggers/tables/index.html.erb +27 -18
  35. data/app/views/pg_sql_triggers/tables/show.html.erb +0 -2
  36. data/app/views/pg_sql_triggers/triggers/_drop_modal.html.erb +0 -0
  37. data/app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb +0 -0
  38. data/app/views/pg_sql_triggers/triggers/show.html.erb +33 -0
  39. data/config/initializers/pg_sql_triggers.rb +0 -0
  40. data/config/routes.rb +0 -14
  41. data/db/migrate/{20251222000001_create_pg_sql_triggers_tables.rb → 20251222104327_create_pg_sql_triggers_tables.rb} +0 -0
  42. data/db/migrate/20251229071916_add_timing_to_pg_sql_triggers_registry.rb +0 -0
  43. data/db/migrate/{20260103000001_create_pg_sql_triggers_audit_log.rb → 20260103114508_create_pg_sql_triggers_audit_log.rb} +0 -0
  44. data/db/migrate/20260228162233_add_for_each_to_pg_sql_triggers_registry.rb +8 -0
  45. data/db/migrate/20260412185841_add_constraint_deferral_to_pg_sql_triggers_registry.rb +9 -0
  46. data/docs/README.md +3 -0
  47. data/docs/api-reference.md +176 -152
  48. data/docs/audit-trail.md +1 -1
  49. data/docs/configuration.md +196 -3
  50. data/docs/getting-started.md +31 -16
  51. data/docs/kill-switch.md +0 -0
  52. data/docs/permissions.md +6 -9
  53. data/docs/troubleshooting.md +0 -0
  54. data/docs/ui-guide.md +0 -0
  55. data/docs/usage-guide.md +112 -67
  56. data/docs/web-ui.md +3 -103
  57. data/lib/generators/pg_sql_triggers/install_generator.rb +0 -0
  58. data/lib/generators/pg_sql_triggers/templates/README +0 -0
  59. data/lib/generators/pg_sql_triggers/templates/create_pg_sql_triggers_tables.rb +0 -0
  60. data/lib/generators/pg_sql_triggers/templates/initializer.rb +14 -0
  61. data/lib/generators/pg_sql_triggers/templates/trigger_dsl.rb.tt +11 -0
  62. data/lib/generators/pg_sql_triggers/templates/trigger_migration.rb.erb +0 -0
  63. data/lib/generators/pg_sql_triggers/templates/trigger_migration_full.rb.tt +29 -0
  64. data/lib/generators/pg_sql_triggers/trigger_generator.rb +83 -0
  65. data/lib/generators/pg_sql_triggers/trigger_migration_generator.rb +0 -0
  66. data/lib/pg_sql_triggers/alerting.rb +77 -0
  67. data/lib/pg_sql_triggers/database_introspection.rb +0 -0
  68. data/lib/pg_sql_triggers/deferral_checksum.rb +54 -0
  69. data/lib/pg_sql_triggers/drift/db_queries.rb +26 -13
  70. data/lib/pg_sql_triggers/drift/detector.rb +59 -38
  71. data/lib/pg_sql_triggers/drift/reporter.rb +0 -0
  72. data/lib/pg_sql_triggers/drift.rb +5 -0
  73. data/lib/pg_sql_triggers/dsl/trigger_definition.rb +68 -20
  74. data/lib/pg_sql_triggers/dsl.rb +0 -0
  75. data/lib/pg_sql_triggers/engine.rb +49 -0
  76. data/lib/pg_sql_triggers/errors.rb +0 -0
  77. data/lib/pg_sql_triggers/events_checksum.rb +114 -0
  78. data/lib/pg_sql_triggers/migration.rb +5 -6
  79. data/lib/pg_sql_triggers/migrator/pre_apply_comparator.rb +85 -82
  80. data/lib/pg_sql_triggers/migrator/pre_apply_diff_reporter.rb +0 -0
  81. data/lib/pg_sql_triggers/migrator/safety_validator.rb +34 -12
  82. data/lib/pg_sql_triggers/migrator.rb +137 -94
  83. data/lib/pg_sql_triggers/permissions/checker.rb +12 -15
  84. data/lib/pg_sql_triggers/permissions.rb +1 -0
  85. data/lib/pg_sql_triggers/rake_development_boot.rb +65 -0
  86. data/lib/pg_sql_triggers/registry/manager.rb +60 -21
  87. data/lib/pg_sql_triggers/registry/validator.rb +287 -6
  88. data/lib/pg_sql_triggers/registry.rb +0 -0
  89. data/lib/pg_sql_triggers/schema_dumper_extension.rb +32 -0
  90. data/lib/pg_sql_triggers/sql/kill_switch.rb +154 -275
  91. data/lib/pg_sql_triggers/sql.rb +0 -6
  92. data/lib/pg_sql_triggers/testing/dry_run.rb +0 -0
  93. data/lib/pg_sql_triggers/testing/function_tester.rb +97 -107
  94. data/lib/pg_sql_triggers/testing/safe_executor.rb +0 -0
  95. data/lib/pg_sql_triggers/testing/syntax_validator.rb +0 -0
  96. data/lib/pg_sql_triggers/testing.rb +0 -0
  97. data/lib/pg_sql_triggers/trigger_structure_dumper.rb +111 -0
  98. data/lib/pg_sql_triggers/version.rb +1 -1
  99. data/lib/pg_sql_triggers.rb +21 -1
  100. data/lib/tasks/trigger_migrations.rake +235 -152
  101. data/rakelib/pg_sql_triggers_environment.rake +9 -0
  102. data/scripts/generate_coverage_report.rb +4 -1
  103. data/sig/pg_sql_triggers.rbs +0 -0
  104. metadata +68 -22
  105. data/Goal.md +0 -742
  106. data/app/controllers/pg_sql_triggers/generator_controller.rb +0 -213
  107. data/app/controllers/pg_sql_triggers/sql_capsules_controller.rb +0 -161
  108. data/app/views/pg_sql_triggers/generator/new.html.erb +0 -388
  109. data/app/views/pg_sql_triggers/generator/preview.html.erb +0 -305
  110. data/app/views/pg_sql_triggers/sql_capsules/new.html.erb +0 -81
  111. data/app/views/pg_sql_triggers/sql_capsules/show.html.erb +0 -85
  112. data/lib/generators/trigger/migration_generator.rb +0 -60
  113. data/lib/pg_sql_triggers/generator/form.rb +0 -80
  114. data/lib/pg_sql_triggers/generator/service.rb +0 -339
  115. data/lib/pg_sql_triggers/generator.rb +0 -8
  116. data/lib/pg_sql_triggers/sql/capsule.rb +0 -79
  117. data/lib/pg_sql_triggers/sql/executor.rb +0 -200
@@ -2,327 +2,206 @@
2
2
 
3
3
  module PgSqlTriggers
4
4
  module SQL
5
- # KillSwitch is a centralized safety gate that prevents dangerous operations
6
- # from being executed in protected environments (typically production).
7
- #
8
- # It operates on three levels:
9
- # 1. Configuration Level: Environment-based activation via PgSqlTriggers.kill_switch_enabled
10
- # 2. Runtime Level: ENV variable override support (KILL_SWITCH_OVERRIDE)
11
- # 3. Explicit Confirmation Level: Typed confirmation text for critical operations
12
- #
13
- # @example Basic usage in a dangerous operation
14
- # KillSwitch.check!(
15
- # operation: :migrate_up,
16
- # environment: Rails.env,
17
- # confirmation: params[:confirmation_text],
18
- # actor: { type: 'UI', id: current_user.email }
19
- # )
20
- #
21
- # @example Using override block
22
- # KillSwitch.override(confirmation: "EXECUTE MIGRATE_UP") do
23
- # # dangerous operation here
24
- # end
5
+ # Private helpers for KillSwitch not part of the public API.
6
+ module KillSwitchHelpers
7
+ def validate_confirmation!(confirmation, operation)
8
+ expected = expected_confirmation(operation)
9
+
10
+ if confirmation.nil? || confirmation.strip.empty?
11
+ raise PgSqlTriggers::KillSwitchError.new(
12
+ "Confirmation text required. Expected: '#{expected}'",
13
+ error_code: "KILL_SWITCH_CONFIRMATION_REQUIRED",
14
+ recovery_suggestion: "Provide the confirmation text: #{expected}",
15
+ context: { operation: operation, expected_confirmation: expected }
16
+ )
17
+ end
18
+
19
+ return if confirmation.strip == expected
20
+
21
+ raise PgSqlTriggers::KillSwitchError.new(
22
+ "Invalid confirmation text. Expected: '#{expected}', got: '#{confirmation.strip}'",
23
+ error_code: "KILL_SWITCH_CONFIRMATION_INVALID",
24
+ recovery_suggestion: "Use the exact confirmation text: #{expected}",
25
+ context: {
26
+ operation: operation,
27
+ expected_confirmation: expected,
28
+ provided_confirmation: confirmation.strip
29
+ }
30
+ )
31
+ end
32
+
33
+ def kill_switch_enabled?
34
+ return true unless PgSqlTriggers.respond_to?(:kill_switch_enabled)
35
+
36
+ value = PgSqlTriggers.kill_switch_enabled
37
+ value.nil? || value
38
+ end
39
+
40
+ def protected_environment?(env)
41
+ return false if env.nil?
42
+
43
+ configured = PgSqlTriggers.kill_switch_environments if PgSqlTriggers.respond_to?(:kill_switch_environments)
44
+ Array(configured || %i[production staging]).map(&:to_s).include?(env.to_s)
45
+ end
46
+
47
+ def resolve_environment(environment)
48
+ return environment.to_s if environment.present?
49
+ return Rails.env.to_s if defined?(Rails) && Rails.respond_to?(:env)
50
+
51
+ if PgSqlTriggers.respond_to?(:default_environment) && PgSqlTriggers.default_environment.respond_to?(:call)
52
+ begin
53
+ return PgSqlTriggers.default_environment.call.to_s
54
+ rescue NameError => e
55
+ # NoMethodError inherits from NameError, so both are rescued by this clause.
56
+ logger&.debug "[KILL_SWITCH] Could not resolve default_environment: #{e.message}"
57
+ end
58
+ end
59
+
60
+ ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
61
+ end
62
+
63
+ def confirmation_required?
64
+ return true unless PgSqlTriggers.respond_to?(:kill_switch_confirmation_required)
65
+
66
+ value = PgSqlTriggers.kill_switch_confirmation_required
67
+ value.nil? || value
68
+ end
69
+
70
+ def expected_confirmation(operation)
71
+ if PgSqlTriggers.respond_to?(:kill_switch_confirmation_pattern) &&
72
+ PgSqlTriggers.kill_switch_confirmation_pattern.respond_to?(:call)
73
+ PgSqlTriggers.kill_switch_confirmation_pattern.call(operation)
74
+ else
75
+ "EXECUTE #{operation.to_s.upcase}"
76
+ end
77
+ end
78
+
79
+ def logger
80
+ if PgSqlTriggers.respond_to?(:kill_switch_logger)
81
+ PgSqlTriggers.kill_switch_logger
82
+ elsif defined?(Rails) && Rails.respond_to?(:logger)
83
+ Rails.logger
84
+ end
85
+ end
86
+
87
+ def log(level, status, **context)
88
+ actor = context[:actor]
89
+ actor_str = if actor.is_a?(Hash)
90
+ "#{actor[:type] || 'unknown'}:#{actor[:id] || 'unknown'}"
91
+ else
92
+ actor&.to_s || "unknown"
93
+ end
94
+ msg = "[KILL_SWITCH] #{status}: operation=#{context[:operation]} " \
95
+ "environment=#{context[:environment]} actor=#{actor_str}"
96
+ msg = "#{msg} #{context[:extra]}" if context[:extra]
97
+ logger&.public_send(level, msg)
98
+ end
99
+ end
100
+ private_constant :KillSwitchHelpers
101
+
102
+ # KillSwitch: three-layer safety gate for dangerous operations.
25
103
  #
26
- # @example CLI usage with ENV override
27
- # KILL_SWITCH_OVERRIDE=true CONFIRMATION_TEXT="EXECUTE MIGRATE_UP" rake pg_sql_triggers:migrate
104
+ # Layer 1 config: PgSqlTriggers.kill_switch_enabled / kill_switch_environments
105
+ # Layer 2 – ENV: KILL_SWITCH_OVERRIDE=true (+ optional confirmation)
106
+ # Layer 3 – explicit: confirmation text passed directly to check!
28
107
  #
29
- # rubocop:disable Metrics/ModuleLength
108
+ # @example
109
+ # KillSwitch.check!(operation: :migrate_up, environment: Rails.env,
110
+ # confirmation: params[:confirmation_text],
111
+ # actor: { type: "UI", id: current_user.email })
30
112
  module KillSwitch
31
- # Thread-local storage key for override state
32
113
  OVERRIDE_KEY = :pg_sql_triggers_kill_switch_override
33
114
 
34
115
  class << self
35
- # Checks if the kill switch is active for the given environment and operation.
36
- #
37
- # @param environment [String, Symbol, nil] The environment to check (defaults to current environment)
38
- # @param operation [String, Symbol, nil] The operation being performed (for logging)
39
- # @return [Boolean] true if kill switch is active, false otherwise
40
- def active?(environment: nil, operation: nil)
41
- # Check if kill switch is globally disabled
42
- return false unless kill_switch_enabled?
43
-
44
- # Detect environment
45
- env = detect_environment(environment)
116
+ include KillSwitchHelpers
46
117
 
47
- # Check if this environment is protected
48
- is_active = protected_environment?(env)
118
+ private :kill_switch_enabled?, :protected_environment?,
119
+ :resolve_environment, :confirmation_required?, :expected_confirmation, :logger, :log
49
120
 
50
- # Log the check if logger is available and operation is provided
51
- if logger && operation
52
- logger.debug "[KILL_SWITCH] Check: operation=#{operation} environment=#{env} active=#{is_active}"
53
- end
121
+ def active?(environment: nil, operation: nil)
122
+ return false unless kill_switch_enabled?
54
123
 
55
- is_active
124
+ env = resolve_environment(environment)
125
+ active = protected_environment?(env)
126
+ logger&.debug "[KILL_SWITCH] Check: operation=#{operation} environment=#{env} active=#{active}" if operation
127
+ active
56
128
  end
57
129
 
58
- # Checks if an operation should be blocked by the kill switch.
59
- # Raises KillSwitchError if the operation is blocked.
60
- #
61
- # @param operation [String, Symbol] The operation being performed
62
- # @param environment [String, Symbol, nil] The environment (defaults to current)
63
- # @param confirmation [String, nil] The confirmation text provided by the user
64
- # @param actor [Hash, nil] Information about who is performing the operation
65
- # @raise [PgSqlTriggers::KillSwitchError] if the operation is blocked
66
- # @return [void]
67
130
  def check!(operation:, environment: nil, confirmation: nil, actor: nil)
68
- env = detect_environment(environment)
131
+ env = resolve_environment(environment)
69
132
 
70
- # Check if kill switch is active for this environment
71
133
  unless active?(environment: env, operation: operation)
72
- log_allowed(operation: operation, environment: env, actor: actor, reason: "not_protected_environment")
134
+ log(:info, "ALLOWED",
135
+ operation: operation,
136
+ environment: env,
137
+ actor: actor,
138
+ extra: "reason=not_protected_environment")
73
139
  return
74
140
  end
75
141
 
76
- # Check for thread-local override
77
- if thread_override_active?
78
- log_override(operation: operation, environment: env, actor: actor, source: "thread_local")
142
+ if Thread.current[OVERRIDE_KEY]
143
+ log(:warn, "OVERRIDDEN",
144
+ operation: operation,
145
+ environment: env,
146
+ actor: actor,
147
+ extra: "source=thread_local")
79
148
  return
80
149
  end
81
150
 
82
- # Check for ENV override
83
- if env_override_active?
84
- # If ENV override is present, check confirmation if required
151
+ if ENV["KILL_SWITCH_OVERRIDE"]&.downcase == "true"
85
152
  if confirmation_required?
86
153
  validate_confirmation!(confirmation, operation)
87
- log_override(operation: operation, environment: env, actor: actor, source: "env_with_confirmation",
88
- confirmation: confirmation)
154
+ log(:warn, "OVERRIDDEN",
155
+ operation: operation,
156
+ environment: env,
157
+ actor: actor,
158
+ extra: "source=env_with_confirmation confirmation=#{confirmation}")
89
159
  else
90
- log_override(operation: operation, environment: env, actor: actor, source: "env_without_confirmation")
160
+ log(:warn, "OVERRIDDEN",
161
+ operation: operation,
162
+ environment: env,
163
+ actor: actor,
164
+ extra: "source=env_without_confirmation")
91
165
  end
92
166
  return
93
167
  end
94
168
 
95
- # If confirmation is provided, validate it
96
169
  unless confirmation.nil?
97
170
  validate_confirmation!(confirmation, operation)
98
- log_override(operation: operation, environment: env, actor: actor, source: "explicit_confirmation",
99
- confirmation: confirmation)
171
+ log(:warn, "OVERRIDDEN",
172
+ operation: operation,
173
+ environment: env,
174
+ actor: actor,
175
+ extra: "source=explicit_confirmation confirmation=#{confirmation}")
100
176
  return
101
177
  end
102
178
 
103
- # No override mechanism satisfied - block the operation
104
- log_blocked(operation: operation, environment: env, actor: actor)
105
- raise_blocked_error(operation: operation, environment: env)
179
+ log(:error, "BLOCKED", operation: operation, environment: env, actor: actor)
180
+ expected = expected_confirmation(operation)
181
+ raise PgSqlTriggers::KillSwitchError.new(
182
+ "Kill switch is active for #{env} environment. Operation '#{operation}' has been blocked.\n\n" \
183
+ "To override: KILL_SWITCH_OVERRIDE=true or provide confirmation text: #{expected}",
184
+ error_code: "KILL_SWITCH_ACTIVE",
185
+ recovery_suggestion: "Provide the confirmation text: #{expected}",
186
+ context: { operation: operation, environment: env, expected_confirmation: expected }
187
+ )
106
188
  end
107
189
 
108
- # Temporarily overrides the kill switch for the duration of the block.
109
- # Uses thread-local storage to ensure thread safety.
110
- #
111
- # @param confirmation [String, nil] Optional confirmation text
112
- # @yield The block to execute with kill switch overridden
113
- # @return The return value of the block
114
190
  def override(confirmation: nil)
115
191
  raise ArgumentError, "Block required for kill switch override" unless block_given?
116
192
 
117
- # Validate confirmation if provided and required
118
- if confirmation.present? && confirmation_required?
119
- # NOTE: We can't validate against a specific operation here since we don't know it
120
- # The block itself will call check! with the operation, which will see the override
193
+ if confirmation.present?
121
194
  logger&.info "[KILL_SWITCH] Override block initiated with confirmation: #{confirmation}"
122
195
  end
123
-
124
- # Set thread-local override
125
- previous_value = Thread.current[OVERRIDE_KEY]
196
+ previous = Thread.current[OVERRIDE_KEY]
126
197
  Thread.current[OVERRIDE_KEY] = true
127
-
128
198
  begin
129
199
  yield
130
200
  ensure
131
- # Restore previous value
132
- Thread.current[OVERRIDE_KEY] = previous_value
133
- end
134
- end
135
-
136
- # Validates the confirmation text against the expected pattern for the operation.
137
- #
138
- # @param confirmation [String, nil] The confirmation text to validate
139
- # @param operation [String, Symbol] The operation being confirmed
140
- # @raise [PgSqlTriggers::KillSwitchError] if confirmation is invalid
141
- # @return [void]
142
- def validate_confirmation!(confirmation, operation)
143
- expected = expected_confirmation(operation)
144
-
145
- if confirmation.nil? || confirmation.strip.empty?
146
- raise PgSqlTriggers::KillSwitchError.new(
147
- "Confirmation text required. Expected: '#{expected}'",
148
- error_code: "KILL_SWITCH_CONFIRMATION_REQUIRED",
149
- recovery_suggestion: "Provide the confirmation text: #{expected}",
150
- context: { operation: operation, expected_confirmation: expected }
151
- )
201
+ Thread.current[OVERRIDE_KEY] = previous
152
202
  end
153
-
154
- return if confirmation.strip == expected
155
-
156
- raise PgSqlTriggers::KillSwitchError.new(
157
- "Invalid confirmation text. Expected: '#{expected}', got: '#{confirmation.strip}'",
158
- error_code: "KILL_SWITCH_CONFIRMATION_INVALID",
159
- recovery_suggestion: "Use the exact confirmation text: #{expected}",
160
- context: { operation: operation, expected_confirmation: expected,
161
- provided_confirmation: confirmation.strip }
162
- )
163
- end
164
-
165
- private
166
-
167
- # Checks if kill switch is globally enabled in configuration
168
- def kill_switch_enabled?
169
- return true unless PgSqlTriggers.respond_to?(:kill_switch_enabled)
170
-
171
- # Default to true (fail-safe) if not configured
172
- value = PgSqlTriggers.kill_switch_enabled
173
- value.nil? || value
174
- end
175
-
176
- # Checks if the given environment is protected by the kill switch
177
- def protected_environment?(environment)
178
- return false if environment.nil?
179
-
180
- protected_envs = if PgSqlTriggers.respond_to?(:kill_switch_environments)
181
- value = PgSqlTriggers.kill_switch_environments
182
- value.nil? ? %i[production staging] : value
183
- else
184
- %i[production staging]
185
- end
186
-
187
- protected_envs = Array(protected_envs).map(&:to_s)
188
- protected_envs.include?(environment.to_s)
189
- end
190
-
191
- # Detects the current environment
192
- def detect_environment(environment)
193
- return environment.to_s if environment.present?
194
-
195
- # Try Rails environment
196
- return Rails.env.to_s if defined?(Rails) && Rails.respond_to?(:env)
197
-
198
- # Try PgSqlTriggers default_environment
199
- if PgSqlTriggers.respond_to?(:default_environment) && PgSqlTriggers.default_environment.respond_to?(:call)
200
- begin
201
- return PgSqlTriggers.default_environment.call.to_s
202
- rescue NameError, NoMethodError # rubocop:disable Lint/ShadowedException
203
- # Fall through to next option if default_environment proc references undefined constants
204
- end
205
- end
206
-
207
- # Fall back to RAILS_ENV or RACK_ENV
208
- ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
209
- end
210
-
211
- # Checks if thread-local override is active
212
- def thread_override_active?
213
- Thread.current[OVERRIDE_KEY] == true
214
- end
215
-
216
- # Checks if ENV override is active
217
- def env_override_active?
218
- ENV["KILL_SWITCH_OVERRIDE"]&.downcase == "true"
219
- end
220
-
221
- # Checks if confirmation is required for overrides
222
- def confirmation_required?
223
- return true unless PgSqlTriggers.respond_to?(:kill_switch_confirmation_required)
224
-
225
- # Default to true (safer) if not configured
226
- value = PgSqlTriggers.kill_switch_confirmation_required
227
- value.nil? || value
228
- end
229
-
230
- # Generates the expected confirmation text for an operation
231
- def expected_confirmation(operation)
232
- if PgSqlTriggers.respond_to?(:kill_switch_confirmation_pattern) &&
233
- PgSqlTriggers.kill_switch_confirmation_pattern.respond_to?(:call)
234
- PgSqlTriggers.kill_switch_confirmation_pattern.call(operation)
235
- else
236
- # Default pattern
237
- "EXECUTE #{operation.to_s.upcase}"
238
- end
239
- end
240
-
241
- # Returns the configured logger
242
- def logger
243
- if PgSqlTriggers.respond_to?(:kill_switch_logger)
244
- PgSqlTriggers.kill_switch_logger
245
- elsif defined?(Rails) && Rails.respond_to?(:logger)
246
- Rails.logger
247
- end
248
- end
249
-
250
- # Logs an allowed operation
251
- def log_allowed(operation:, environment:, actor:, reason:)
252
- actor_info = format_actor(actor)
253
- logger&.info "[KILL_SWITCH] ALLOWED: operation=#{operation} environment=#{environment} " \
254
- "actor=#{actor_info} reason=#{reason}"
255
- end
256
-
257
- # Logs an overridden operation
258
- def log_override(operation:, environment:, actor:, source:, confirmation: nil)
259
- actor_info = format_actor(actor)
260
- conf_info = confirmation ? " confirmation=#{confirmation}" : ""
261
- logger&.warn "[KILL_SWITCH] OVERRIDDEN: operation=#{operation} environment=#{environment} " \
262
- "actor=#{actor_info} source=#{source}#{conf_info}"
263
- end
264
-
265
- # Logs a blocked operation
266
- def log_blocked(operation:, environment:, actor:)
267
- actor_info = format_actor(actor)
268
- logger&.error "[KILL_SWITCH] BLOCKED: operation=#{operation} environment=#{environment} actor=#{actor_info}"
269
- end
270
-
271
- # Formats actor information for logging
272
- def format_actor(actor)
273
- return "unknown" if actor.nil?
274
- return actor.to_s unless actor.is_a?(Hash)
275
-
276
- "#{actor[:type] || 'unknown'}:#{actor[:id] || 'unknown'}"
277
- end
278
-
279
- # Raises a kill switch error with helpful message
280
- def raise_blocked_error(operation:, environment:)
281
- expected = expected_confirmation(operation)
282
-
283
- message = <<~ERROR
284
- Kill switch is active for #{environment} environment.
285
- Operation '#{operation}' has been blocked for safety.
286
-
287
- To override this protection, you must provide confirmation.
288
-
289
- For CLI/rake tasks, use:
290
- KILL_SWITCH_OVERRIDE=true CONFIRMATION_TEXT="#{expected}" rake your:task
291
-
292
- For console operations, use:
293
- PgSqlTriggers::SQL::KillSwitch.override(confirmation: "#{expected}") do
294
- # your dangerous operation here
295
- end
296
-
297
- For UI operations, enter the confirmation text: #{expected}
298
-
299
- This protection prevents accidental destructive operations in production.
300
- Make sure you understand the implications before proceeding.
301
- ERROR
302
-
303
- recovery = <<~RECOVERY
304
- To override, provide the confirmation text: #{expected}
305
-
306
- For CLI/rake tasks:
307
- KILL_SWITCH_OVERRIDE=true CONFIRMATION_TEXT="#{expected}" rake your:task
308
-
309
- For console operations:
310
- PgSqlTriggers::SQL::KillSwitch.override(confirmation: "#{expected}") do
311
- # your operation here
312
- end
313
-
314
- For UI operations, enter the confirmation text in the modal.
315
- RECOVERY
316
-
317
- raise PgSqlTriggers::KillSwitchError.new(
318
- message,
319
- error_code: "KILL_SWITCH_ACTIVE",
320
- recovery_suggestion: recovery.strip,
321
- context: { operation: operation, environment: environment, expected_confirmation: expected }
322
- )
323
203
  end
324
204
  end
325
205
  end
326
- # rubocop:enable Metrics/ModuleLength
327
206
  end
328
207
  end
@@ -2,14 +2,8 @@
2
2
 
3
3
  module PgSqlTriggers
4
4
  module SQL
5
- autoload :Capsule, "pg_sql_triggers/sql/capsule"
6
- autoload :Executor, "pg_sql_triggers/sql/executor"
7
5
  autoload :KillSwitch, "pg_sql_triggers/sql/kill_switch"
8
6
 
9
- def self.execute_capsule(capsule_name, **options)
10
- Executor.execute_capsule(capsule_name, **options)
11
- end
12
-
13
7
  def self.kill_switch_active?
14
8
  KillSwitch.active?
15
9
  end
File without changes