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.
- checksums.yaml +4 -4
- data/.erb_lint.yml +0 -0
- data/.rspec +0 -0
- data/.rubocop.yml +6 -16
- data/AGENTS.md +8 -0
- data/CHANGELOG.md +354 -0
- data/COVERAGE.md +39 -41
- data/LICENSE +0 -0
- data/README.md +44 -26
- data/RELEASE.md +0 -0
- data/Rakefile +5 -0
- data/app/assets/javascripts/pg_sql_triggers/application.js +0 -0
- data/app/assets/javascripts/pg_sql_triggers/trigger_actions.js +0 -0
- data/app/assets/stylesheets/pg_sql_triggers/application.css +0 -0
- data/app/controllers/concerns/pg_sql_triggers/error_handling.rb +0 -0
- data/app/controllers/concerns/pg_sql_triggers/kill_switch_protection.rb +0 -0
- data/app/controllers/concerns/pg_sql_triggers/permission_checking.rb +6 -5
- data/app/controllers/pg_sql_triggers/application_controller.rb +0 -0
- data/app/controllers/pg_sql_triggers/audit_logs_controller.rb +81 -64
- data/app/controllers/pg_sql_triggers/dashboard_controller.rb +111 -34
- data/app/controllers/pg_sql_triggers/migrations_controller.rb +13 -14
- data/app/controllers/pg_sql_triggers/tables_controller.rb +8 -0
- data/app/controllers/pg_sql_triggers/triggers_controller.rb +1 -0
- data/app/helpers/pg_sql_triggers/dashboard_helper.rb +19 -0
- data/app/helpers/pg_sql_triggers/permissions_helper.rb +3 -2
- data/app/models/pg_sql_triggers/application_record.rb +0 -0
- data/app/models/pg_sql_triggers/audit_log.rb +29 -47
- data/app/models/pg_sql_triggers/trigger_registry.rb +137 -74
- data/app/views/layouts/pg_sql_triggers/application.html.erb +0 -1
- data/app/views/pg_sql_triggers/audit_logs/index.html.erb +9 -5
- data/app/views/pg_sql_triggers/dashboard/index.html.erb +107 -27
- data/app/views/pg_sql_triggers/shared/_confirmation_modal.html.erb +0 -0
- data/app/views/pg_sql_triggers/shared/_kill_switch_status.html.erb +0 -0
- data/app/views/pg_sql_triggers/tables/index.html.erb +27 -18
- data/app/views/pg_sql_triggers/tables/show.html.erb +0 -2
- data/app/views/pg_sql_triggers/triggers/_drop_modal.html.erb +0 -0
- data/app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb +0 -0
- data/app/views/pg_sql_triggers/triggers/show.html.erb +33 -0
- data/config/initializers/pg_sql_triggers.rb +0 -0
- data/config/routes.rb +0 -14
- data/db/migrate/{20251222000001_create_pg_sql_triggers_tables.rb → 20251222104327_create_pg_sql_triggers_tables.rb} +0 -0
- data/db/migrate/20251229071916_add_timing_to_pg_sql_triggers_registry.rb +0 -0
- data/db/migrate/{20260103000001_create_pg_sql_triggers_audit_log.rb → 20260103114508_create_pg_sql_triggers_audit_log.rb} +0 -0
- data/db/migrate/20260228162233_add_for_each_to_pg_sql_triggers_registry.rb +8 -0
- data/db/migrate/20260412185841_add_constraint_deferral_to_pg_sql_triggers_registry.rb +9 -0
- data/docs/README.md +3 -0
- data/docs/api-reference.md +176 -152
- data/docs/audit-trail.md +1 -1
- data/docs/configuration.md +196 -3
- data/docs/getting-started.md +31 -16
- data/docs/kill-switch.md +0 -0
- data/docs/permissions.md +6 -9
- data/docs/troubleshooting.md +0 -0
- data/docs/ui-guide.md +0 -0
- data/docs/usage-guide.md +112 -67
- data/docs/web-ui.md +3 -103
- data/lib/generators/pg_sql_triggers/install_generator.rb +0 -0
- data/lib/generators/pg_sql_triggers/templates/README +0 -0
- data/lib/generators/pg_sql_triggers/templates/create_pg_sql_triggers_tables.rb +0 -0
- data/lib/generators/pg_sql_triggers/templates/initializer.rb +14 -0
- data/lib/generators/pg_sql_triggers/templates/trigger_dsl.rb.tt +11 -0
- data/lib/generators/pg_sql_triggers/templates/trigger_migration.rb.erb +0 -0
- data/lib/generators/pg_sql_triggers/templates/trigger_migration_full.rb.tt +29 -0
- data/lib/generators/pg_sql_triggers/trigger_generator.rb +83 -0
- data/lib/generators/pg_sql_triggers/trigger_migration_generator.rb +0 -0
- data/lib/pg_sql_triggers/alerting.rb +77 -0
- data/lib/pg_sql_triggers/database_introspection.rb +0 -0
- data/lib/pg_sql_triggers/deferral_checksum.rb +54 -0
- data/lib/pg_sql_triggers/drift/db_queries.rb +26 -13
- data/lib/pg_sql_triggers/drift/detector.rb +59 -38
- data/lib/pg_sql_triggers/drift/reporter.rb +0 -0
- data/lib/pg_sql_triggers/drift.rb +5 -0
- data/lib/pg_sql_triggers/dsl/trigger_definition.rb +68 -20
- data/lib/pg_sql_triggers/dsl.rb +0 -0
- data/lib/pg_sql_triggers/engine.rb +49 -0
- data/lib/pg_sql_triggers/errors.rb +0 -0
- data/lib/pg_sql_triggers/events_checksum.rb +114 -0
- data/lib/pg_sql_triggers/migration.rb +5 -6
- data/lib/pg_sql_triggers/migrator/pre_apply_comparator.rb +85 -82
- data/lib/pg_sql_triggers/migrator/pre_apply_diff_reporter.rb +0 -0
- data/lib/pg_sql_triggers/migrator/safety_validator.rb +34 -12
- data/lib/pg_sql_triggers/migrator.rb +137 -94
- data/lib/pg_sql_triggers/permissions/checker.rb +12 -15
- data/lib/pg_sql_triggers/permissions.rb +1 -0
- data/lib/pg_sql_triggers/rake_development_boot.rb +65 -0
- data/lib/pg_sql_triggers/registry/manager.rb +60 -21
- data/lib/pg_sql_triggers/registry/validator.rb +287 -6
- data/lib/pg_sql_triggers/registry.rb +0 -0
- data/lib/pg_sql_triggers/schema_dumper_extension.rb +32 -0
- data/lib/pg_sql_triggers/sql/kill_switch.rb +154 -275
- data/lib/pg_sql_triggers/sql.rb +0 -6
- data/lib/pg_sql_triggers/testing/dry_run.rb +0 -0
- data/lib/pg_sql_triggers/testing/function_tester.rb +97 -107
- data/lib/pg_sql_triggers/testing/safe_executor.rb +0 -0
- data/lib/pg_sql_triggers/testing/syntax_validator.rb +0 -0
- data/lib/pg_sql_triggers/testing.rb +0 -0
- data/lib/pg_sql_triggers/trigger_structure_dumper.rb +111 -0
- data/lib/pg_sql_triggers/version.rb +1 -1
- data/lib/pg_sql_triggers.rb +21 -1
- data/lib/tasks/trigger_migrations.rake +235 -152
- data/rakelib/pg_sql_triggers_environment.rake +9 -0
- data/scripts/generate_coverage_report.rb +4 -1
- data/sig/pg_sql_triggers.rbs +0 -0
- metadata +68 -22
- data/Goal.md +0 -742
- data/app/controllers/pg_sql_triggers/generator_controller.rb +0 -213
- data/app/controllers/pg_sql_triggers/sql_capsules_controller.rb +0 -161
- data/app/views/pg_sql_triggers/generator/new.html.erb +0 -388
- data/app/views/pg_sql_triggers/generator/preview.html.erb +0 -305
- data/app/views/pg_sql_triggers/sql_capsules/new.html.erb +0 -81
- data/app/views/pg_sql_triggers/sql_capsules/show.html.erb +0 -85
- data/lib/generators/trigger/migration_generator.rb +0 -60
- data/lib/pg_sql_triggers/generator/form.rb +0 -80
- data/lib/pg_sql_triggers/generator/service.rb +0 -339
- data/lib/pg_sql_triggers/generator.rb +0 -8
- data/lib/pg_sql_triggers/sql/capsule.rb +0 -79
- 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
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
#
|
|
27
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
118
|
+
private :kill_switch_enabled?, :protected_environment?,
|
|
119
|
+
:resolve_environment, :confirmation_required?, :expected_confirmation, :logger, :log
|
|
49
120
|
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/pg_sql_triggers/sql.rb
CHANGED
|
@@ -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
|