pg_sql_triggers 1.2.0 → 1.3.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/CHANGELOG.md +144 -0
- data/COVERAGE.md +26 -19
- data/Goal.md +276 -155
- data/README.md +27 -1
- data/app/assets/javascripts/pg_sql_triggers/trigger_actions.js +50 -0
- data/app/controllers/concerns/pg_sql_triggers/error_handling.rb +56 -0
- data/app/controllers/concerns/pg_sql_triggers/kill_switch_protection.rb +66 -0
- data/app/controllers/concerns/pg_sql_triggers/permission_checking.rb +117 -0
- data/app/controllers/pg_sql_triggers/application_controller.rb +10 -62
- data/app/controllers/pg_sql_triggers/audit_logs_controller.rb +102 -0
- data/app/controllers/pg_sql_triggers/dashboard_controller.rb +4 -9
- data/app/controllers/pg_sql_triggers/tables_controller.rb +30 -4
- data/app/controllers/pg_sql_triggers/triggers_controller.rb +3 -21
- data/app/helpers/pg_sql_triggers/permissions_helper.rb +43 -0
- data/app/models/pg_sql_triggers/audit_log.rb +106 -0
- data/app/models/pg_sql_triggers/trigger_registry.rb +178 -9
- data/app/views/layouts/pg_sql_triggers/application.html.erb +26 -6
- data/app/views/pg_sql_triggers/audit_logs/index.html.erb +177 -0
- data/app/views/pg_sql_triggers/dashboard/index.html.erb +33 -8
- data/app/views/pg_sql_triggers/tables/index.html.erb +76 -3
- data/app/views/pg_sql_triggers/tables/show.html.erb +17 -4
- data/app/views/pg_sql_triggers/triggers/_drop_modal.html.erb +16 -7
- data/app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb +16 -7
- data/app/views/pg_sql_triggers/triggers/show.html.erb +26 -6
- data/config/routes.rb +2 -0
- data/db/migrate/20260103000001_create_pg_sql_triggers_audit_log.rb +28 -0
- data/docs/README.md +15 -5
- data/docs/api-reference.md +191 -0
- data/docs/audit-trail.md +413 -0
- data/docs/configuration.md +6 -6
- data/docs/permissions.md +369 -0
- data/docs/troubleshooting.md +486 -0
- data/docs/ui-guide.md +211 -0
- data/docs/web-ui.md +257 -34
- data/lib/pg_sql_triggers/errors.rb +245 -0
- data/lib/pg_sql_triggers/generator/service.rb +32 -0
- data/lib/pg_sql_triggers/permissions/checker.rb +9 -2
- data/lib/pg_sql_triggers/registry.rb +141 -8
- data/lib/pg_sql_triggers/sql/kill_switch.rb +33 -5
- data/lib/pg_sql_triggers/testing/function_tester.rb +2 -0
- data/lib/pg_sql_triggers/version.rb +1 -1
- data/lib/pg_sql_triggers.rb +3 -6
- metadata +29 -6
- data/docs/screenshots/.gitkeep +0 -1
- data/docs/screenshots/Generate Trigger.png +0 -0
- data/docs/screenshots/Triggers Page.png +0 -0
- data/docs/screenshots/kill error.png +0 -0
- data/docs/screenshots/kill modal for migration down.png +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgSqlTriggers
|
|
4
|
+
# Audit log model for tracking all trigger operations
|
|
5
|
+
class AuditLog < PgSqlTriggers::ApplicationRecord
|
|
6
|
+
self.table_name = "pg_sql_triggers_audit_log"
|
|
7
|
+
|
|
8
|
+
# Scopes
|
|
9
|
+
scope :for_trigger, ->(trigger_name) { where(trigger_name: trigger_name) }
|
|
10
|
+
scope :for_operation, ->(operation) { where(operation: operation) }
|
|
11
|
+
scope :for_environment, ->(env) { where(environment: env) }
|
|
12
|
+
scope :successful, -> { where(status: "success") }
|
|
13
|
+
scope :failed, -> { where(status: "failure") }
|
|
14
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
15
|
+
|
|
16
|
+
# Validations
|
|
17
|
+
validates :operation, presence: true
|
|
18
|
+
validates :status, presence: true, inclusion: { in: %w[success failure] }
|
|
19
|
+
|
|
20
|
+
# Class methods for logging operations
|
|
21
|
+
class << self
|
|
22
|
+
# Log a successful operation
|
|
23
|
+
#
|
|
24
|
+
# @param operation [Symbol, String] The operation being performed
|
|
25
|
+
# @param trigger_name [String, nil] The trigger name (if applicable)
|
|
26
|
+
# @param actor [Hash] Information about who performed the action
|
|
27
|
+
# @param environment [String, nil] The environment
|
|
28
|
+
# @param reason [String, nil] Reason for the operation
|
|
29
|
+
# @param confirmation_text [String, nil] Confirmation text used
|
|
30
|
+
# @param before_state [Hash, nil] State before operation
|
|
31
|
+
# @param after_state [Hash, nil] State after operation
|
|
32
|
+
# @param diff [String, nil] Diff information
|
|
33
|
+
# rubocop:disable Metrics/ParameterLists
|
|
34
|
+
def log_success(operation:, trigger_name: nil, actor: nil, environment: nil,
|
|
35
|
+
reason: nil, confirmation_text: nil, before_state: nil,
|
|
36
|
+
after_state: nil, diff: nil)
|
|
37
|
+
create!(
|
|
38
|
+
trigger_name: trigger_name,
|
|
39
|
+
operation: operation.to_s,
|
|
40
|
+
actor: serialize_actor(actor),
|
|
41
|
+
environment: environment,
|
|
42
|
+
status: "success",
|
|
43
|
+
reason: reason,
|
|
44
|
+
confirmation_text: confirmation_text,
|
|
45
|
+
before_state: before_state,
|
|
46
|
+
after_state: after_state,
|
|
47
|
+
diff: diff
|
|
48
|
+
)
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
Rails.logger.error("Failed to log audit entry: #{e.message}") if defined?(Rails.logger)
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
# rubocop:enable Metrics/ParameterLists
|
|
54
|
+
|
|
55
|
+
# Log a failed operation
|
|
56
|
+
#
|
|
57
|
+
# @param operation [Symbol, String] The operation being performed
|
|
58
|
+
# @param trigger_name [String, nil] The trigger name (if applicable)
|
|
59
|
+
# @param actor [Hash] Information about who performed the action
|
|
60
|
+
# @param environment [String, nil] The environment
|
|
61
|
+
# @param error_message [String] Error message
|
|
62
|
+
# @param reason [String, nil] Reason for the operation (if provided before failure)
|
|
63
|
+
# @param confirmation_text [String, nil] Confirmation text used
|
|
64
|
+
# @param before_state [Hash, nil] State before operation
|
|
65
|
+
# rubocop:disable Metrics/ParameterLists
|
|
66
|
+
def log_failure(operation:, error_message:, trigger_name: nil, actor: nil, environment: nil, reason: nil,
|
|
67
|
+
confirmation_text: nil, before_state: nil)
|
|
68
|
+
create!(
|
|
69
|
+
trigger_name: trigger_name,
|
|
70
|
+
operation: operation.to_s,
|
|
71
|
+
actor: serialize_actor(actor),
|
|
72
|
+
environment: environment,
|
|
73
|
+
status: "failure",
|
|
74
|
+
error_message: error_message,
|
|
75
|
+
reason: reason,
|
|
76
|
+
confirmation_text: confirmation_text,
|
|
77
|
+
before_state: before_state
|
|
78
|
+
)
|
|
79
|
+
rescue StandardError => e
|
|
80
|
+
Rails.logger.error("Failed to log audit entry: #{e.message}") if defined?(Rails.logger)
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
# rubocop:enable Metrics/ParameterLists
|
|
84
|
+
|
|
85
|
+
# Get audit log entries for a specific trigger
|
|
86
|
+
#
|
|
87
|
+
# @param trigger_name [String] The trigger name
|
|
88
|
+
# @return [ActiveRecord::Relation] Audit log entries for the trigger
|
|
89
|
+
def for_trigger_name(trigger_name)
|
|
90
|
+
for_trigger(trigger_name).recent
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def serialize_actor(actor)
|
|
96
|
+
return nil if actor.nil?
|
|
97
|
+
|
|
98
|
+
if actor.is_a?(Hash)
|
|
99
|
+
actor
|
|
100
|
+
else
|
|
101
|
+
{ type: actor.class.name, id: actor.id.to_s }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PgSqlTriggers
|
|
4
|
+
# ActiveRecord model representing a trigger in the registry.
|
|
5
|
+
#
|
|
6
|
+
# This model tracks all triggers managed by pg_sql_triggers, including their
|
|
7
|
+
# state, version, checksum, and drift status.
|
|
8
|
+
#
|
|
9
|
+
# @example Query triggers
|
|
10
|
+
# # Find a trigger
|
|
11
|
+
# trigger = PgSqlTriggers::TriggerRegistry.find_by(trigger_name: "users_email_validation")
|
|
12
|
+
#
|
|
13
|
+
# # Check drift status
|
|
14
|
+
# trigger.drifted? # => true/false
|
|
15
|
+
# trigger.in_sync? # => true/false
|
|
16
|
+
#
|
|
17
|
+
# @example Enable/disable triggers
|
|
18
|
+
# trigger.enable!(actor: current_user, confirmation: "EXECUTE TRIGGER_ENABLE")
|
|
19
|
+
# trigger.disable!(actor: current_user, confirmation: "EXECUTE TRIGGER_DISABLE")
|
|
20
|
+
#
|
|
21
|
+
# @example Drop and re-execute triggers
|
|
22
|
+
# trigger.drop!(reason: "No longer needed", actor: current_user, confirmation: "EXECUTE TRIGGER_DROP")
|
|
23
|
+
# trigger.re_execute!(reason: "Fix drift", actor: current_user, confirmation: "EXECUTE TRIGGER_RE_EXECUTE")
|
|
4
24
|
# rubocop:disable Metrics/ClassLength
|
|
5
25
|
class TriggerRegistry < PgSqlTriggers::ApplicationRecord
|
|
6
26
|
self.table_name = "pg_sql_triggers_registry"
|
|
@@ -19,36 +39,59 @@ module PgSqlTriggers
|
|
|
19
39
|
scope :for_environment, ->(env) { where(environment: [env, nil]) }
|
|
20
40
|
scope :by_source, ->(source) { where(source: source) }
|
|
21
41
|
|
|
22
|
-
#
|
|
42
|
+
# Returns the current drift state of this trigger.
|
|
43
|
+
#
|
|
44
|
+
# @return [String] One of: "in_sync", "drifted", "manual_override", "disabled", "dropped", "unknown"
|
|
23
45
|
def drift_state
|
|
24
46
|
result = PgSqlTriggers::Drift.detect(trigger_name)
|
|
25
47
|
result[:state]
|
|
26
48
|
end
|
|
27
49
|
|
|
50
|
+
# Returns detailed drift detection result for this trigger.
|
|
51
|
+
#
|
|
52
|
+
# @return [Hash] Drift result with keys: :state, :trigger_name, :expected_sql, :actual_sql, etc.
|
|
28
53
|
def drift_result
|
|
29
54
|
PgSqlTriggers::Drift::Detector.detect(trigger_name)
|
|
30
55
|
end
|
|
31
56
|
|
|
57
|
+
# Checks if this trigger has drifted from its expected state.
|
|
58
|
+
#
|
|
59
|
+
# @return [Boolean] true if trigger has drifted, false otherwise
|
|
32
60
|
def drifted?
|
|
33
61
|
drift_state == PgSqlTriggers::DRIFT_STATE_DRIFTED
|
|
34
62
|
end
|
|
35
63
|
|
|
64
|
+
# Checks if this trigger is in sync with its expected state.
|
|
65
|
+
#
|
|
66
|
+
# @return [Boolean] true if trigger is in sync, false otherwise
|
|
36
67
|
def in_sync?
|
|
37
68
|
drift_state == PgSqlTriggers::DRIFT_STATE_IN_SYNC
|
|
38
69
|
end
|
|
39
70
|
|
|
71
|
+
# Checks if this trigger has been dropped from the database.
|
|
72
|
+
#
|
|
73
|
+
# @return [Boolean] true if trigger has been dropped, false otherwise
|
|
40
74
|
def dropped?
|
|
41
75
|
drift_state == PgSqlTriggers::DRIFT_STATE_DROPPED
|
|
42
76
|
end
|
|
43
77
|
|
|
44
|
-
|
|
78
|
+
# Enables this trigger in the database and updates the registry.
|
|
79
|
+
#
|
|
80
|
+
# @param confirmation [String, nil] Optional confirmation text for kill switch protection
|
|
81
|
+
# @param actor [Hash, nil] Information about who is performing the action (must have :type and :id keys)
|
|
82
|
+
# @raise [PgSqlTriggers::KillSwitchError] If kill switch blocks the operation
|
|
83
|
+
# @return [PgSqlTriggers::TriggerRegistry] self
|
|
84
|
+
def enable!(confirmation: nil, actor: nil)
|
|
85
|
+
actor ||= { type: "Console", id: "TriggerRegistry#enable!" }
|
|
86
|
+
before_state = capture_state
|
|
87
|
+
|
|
45
88
|
# Check kill switch before enabling trigger
|
|
46
89
|
# Use Rails.env for kill switch check, not the trigger's environment field
|
|
47
90
|
PgSqlTriggers::SQL::KillSwitch.check!(
|
|
48
91
|
operation: :trigger_enable,
|
|
49
92
|
environment: Rails.env,
|
|
50
93
|
confirmation: confirmation,
|
|
51
|
-
actor:
|
|
94
|
+
actor: actor
|
|
52
95
|
)
|
|
53
96
|
|
|
54
97
|
# Check if trigger exists in database before trying to enable it
|
|
@@ -71,12 +114,16 @@ module PgSqlTriggers
|
|
|
71
114
|
rescue ActiveRecord::StatementInvalid, StandardError => e
|
|
72
115
|
# If trigger doesn't exist or can't be enabled, continue to update registry
|
|
73
116
|
Rails.logger.warn("Could not enable trigger: #{e.message}") if defined?(Rails.logger)
|
|
117
|
+
log_audit_failure(:trigger_enable, actor, e.message, before_state: before_state)
|
|
118
|
+
raise
|
|
74
119
|
end
|
|
75
120
|
end
|
|
76
121
|
|
|
77
122
|
# Update the registry record (always update, even if trigger doesn't exist)
|
|
78
123
|
begin
|
|
79
124
|
update!(enabled: true)
|
|
125
|
+
after_state = capture_state
|
|
126
|
+
log_audit_success(:trigger_enable, actor, before_state: before_state, after_state: after_state)
|
|
80
127
|
rescue ActiveRecord::StatementInvalid, StandardError => e
|
|
81
128
|
# If update! fails, try update_column which bypasses validations and callbacks
|
|
82
129
|
# and might not use execute in the same way
|
|
@@ -85,6 +132,8 @@ module PgSqlTriggers
|
|
|
85
132
|
# rubocop:disable Rails/SkipsModelValidations
|
|
86
133
|
update_column(:enabled, true)
|
|
87
134
|
# rubocop:enable Rails/SkipsModelValidations
|
|
135
|
+
after_state = capture_state
|
|
136
|
+
log_audit_success(:trigger_enable, actor, before_state: before_state, after_state: after_state)
|
|
88
137
|
rescue StandardError => update_error
|
|
89
138
|
# If update_column also fails, just set the in-memory attribute
|
|
90
139
|
# The test might reload, but we've done our best
|
|
@@ -92,18 +141,29 @@ module PgSqlTriggers
|
|
|
92
141
|
Rails.logger.warn("Could not update registry via update_column: #{update_error.message}") if defined?(Rails.logger)
|
|
93
142
|
# rubocop:enable Layout/LineLength
|
|
94
143
|
self.enabled = true
|
|
144
|
+
after_state = capture_state
|
|
145
|
+
log_audit_success(:trigger_enable, actor, before_state: before_state, after_state: after_state)
|
|
95
146
|
end
|
|
96
147
|
end
|
|
97
148
|
end
|
|
98
149
|
|
|
99
|
-
|
|
150
|
+
# Disables this trigger in the database and updates the registry.
|
|
151
|
+
#
|
|
152
|
+
# @param confirmation [String, nil] Optional confirmation text for kill switch protection
|
|
153
|
+
# @param actor [Hash, nil] Information about who is performing the action (must have :type and :id keys)
|
|
154
|
+
# @raise [PgSqlTriggers::KillSwitchError] If kill switch blocks the operation
|
|
155
|
+
# @return [PgSqlTriggers::TriggerRegistry] self
|
|
156
|
+
def disable!(confirmation: nil, actor: nil)
|
|
157
|
+
actor ||= { type: "Console", id: "TriggerRegistry#disable!" }
|
|
158
|
+
before_state = capture_state
|
|
159
|
+
|
|
100
160
|
# Check kill switch before disabling trigger
|
|
101
161
|
# Use Rails.env for kill switch check, not the trigger's environment field
|
|
102
162
|
PgSqlTriggers::SQL::KillSwitch.check!(
|
|
103
163
|
operation: :trigger_disable,
|
|
104
164
|
environment: Rails.env,
|
|
105
165
|
confirmation: confirmation,
|
|
106
|
-
actor:
|
|
166
|
+
actor: actor
|
|
107
167
|
)
|
|
108
168
|
|
|
109
169
|
# Check if trigger exists in database before trying to disable it
|
|
@@ -126,12 +186,16 @@ module PgSqlTriggers
|
|
|
126
186
|
rescue ActiveRecord::StatementInvalid, StandardError => e
|
|
127
187
|
# If trigger doesn't exist or can't be disabled, continue to update registry
|
|
128
188
|
Rails.logger.warn("Could not disable trigger: #{e.message}") if defined?(Rails.logger)
|
|
189
|
+
log_audit_failure(:trigger_disable, actor, e.message, before_state: before_state)
|
|
190
|
+
raise
|
|
129
191
|
end
|
|
130
192
|
end
|
|
131
193
|
|
|
132
194
|
# Update the registry record (always update, even if trigger doesn't exist)
|
|
133
195
|
begin
|
|
134
196
|
update!(enabled: false)
|
|
197
|
+
after_state = capture_state
|
|
198
|
+
log_audit_success(:trigger_disable, actor, before_state: before_state, after_state: after_state)
|
|
135
199
|
rescue ActiveRecord::StatementInvalid, StandardError => e
|
|
136
200
|
# If update! fails, try update_column which bypasses validations and callbacks
|
|
137
201
|
# and might not use execute in the same way
|
|
@@ -140,6 +204,8 @@ module PgSqlTriggers
|
|
|
140
204
|
# rubocop:disable Rails/SkipsModelValidations
|
|
141
205
|
update_column(:enabled, false)
|
|
142
206
|
# rubocop:enable Rails/SkipsModelValidations
|
|
207
|
+
after_state = capture_state
|
|
208
|
+
log_audit_success(:trigger_disable, actor, before_state: before_state, after_state: after_state)
|
|
143
209
|
rescue StandardError => update_error
|
|
144
210
|
# If update_column also fails, just set the in-memory attribute
|
|
145
211
|
# The test might reload, but we've done our best
|
|
@@ -147,17 +213,30 @@ module PgSqlTriggers
|
|
|
147
213
|
Rails.logger.warn("Could not update registry via update_column: #{update_error.message}") if defined?(Rails.logger)
|
|
148
214
|
# rubocop:enable Layout/LineLength
|
|
149
215
|
self.enabled = false
|
|
216
|
+
after_state = capture_state
|
|
217
|
+
log_audit_success(:trigger_disable, actor, before_state: before_state, after_state: after_state)
|
|
150
218
|
end
|
|
151
219
|
end
|
|
152
220
|
end
|
|
153
221
|
|
|
222
|
+
# Drops this trigger from the database and removes it from the registry.
|
|
223
|
+
#
|
|
224
|
+
# @param reason [String] Required reason for dropping the trigger
|
|
225
|
+
# @param confirmation [String, nil] Optional confirmation text for kill switch protection
|
|
226
|
+
# @param actor [Hash, nil] Information about who is performing the action (must have :type and :id keys)
|
|
227
|
+
# @raise [ArgumentError] If reason is missing or empty
|
|
228
|
+
# @raise [PgSqlTriggers::KillSwitchError] If kill switch blocks the operation
|
|
229
|
+
# @return [true] If drop succeeds
|
|
154
230
|
def drop!(reason:, confirmation: nil, actor: nil)
|
|
231
|
+
actor ||= { type: "Console", id: "TriggerRegistry#drop!" }
|
|
232
|
+
before_state = capture_state
|
|
233
|
+
|
|
155
234
|
# Check kill switch before dropping trigger
|
|
156
235
|
PgSqlTriggers::SQL::KillSwitch.check!(
|
|
157
236
|
operation: :trigger_drop,
|
|
158
237
|
environment: Rails.env,
|
|
159
238
|
confirmation: confirmation,
|
|
160
|
-
actor: actor
|
|
239
|
+
actor: actor
|
|
161
240
|
)
|
|
162
241
|
|
|
163
242
|
# Validate reason is provided
|
|
@@ -168,18 +247,41 @@ module PgSqlTriggers
|
|
|
168
247
|
# Execute DROP TRIGGER in transaction
|
|
169
248
|
ActiveRecord::Base.transaction do
|
|
170
249
|
drop_trigger_from_database
|
|
250
|
+
trigger_name
|
|
171
251
|
destroy!
|
|
172
252
|
log_drop_success
|
|
253
|
+
log_audit_success(:trigger_drop, actor, reason: reason, confirmation_text: confirmation,
|
|
254
|
+
before_state: before_state, after_state: { status: "dropped" })
|
|
173
255
|
end
|
|
256
|
+
rescue StandardError => e
|
|
257
|
+
log_audit_failure(:trigger_drop, actor, e.message, reason: reason,
|
|
258
|
+
confirmation_text: confirmation, before_state: before_state)
|
|
259
|
+
raise
|
|
174
260
|
end
|
|
175
261
|
|
|
262
|
+
# Re-executes this trigger by dropping and recreating it.
|
|
263
|
+
#
|
|
264
|
+
# @param reason [String] Required reason for re-executing the trigger
|
|
265
|
+
# @param confirmation [String, nil] Optional confirmation text for kill switch protection
|
|
266
|
+
# @param actor [Hash, nil] Information about who is performing the action (must have :type and :id keys)
|
|
267
|
+
# @raise [ArgumentError] If reason is missing or empty, or if function_body is blank
|
|
268
|
+
# @raise [PgSqlTriggers::KillSwitchError] If kill switch blocks the operation
|
|
269
|
+
# @return [PgSqlTriggers::TriggerRegistry] self
|
|
176
270
|
def re_execute!(reason:, confirmation: nil, actor: nil)
|
|
271
|
+
actor ||= { type: "Console", id: "TriggerRegistry#re_execute!" }
|
|
272
|
+
before_state = capture_state
|
|
273
|
+
drift_info = begin
|
|
274
|
+
drift_result
|
|
275
|
+
rescue StandardError
|
|
276
|
+
nil
|
|
277
|
+
end
|
|
278
|
+
|
|
177
279
|
# Check kill switch before re-executing trigger
|
|
178
280
|
PgSqlTriggers::SQL::KillSwitch.check!(
|
|
179
281
|
operation: :trigger_re_execute,
|
|
180
282
|
environment: Rails.env,
|
|
181
283
|
confirmation: confirmation,
|
|
182
|
-
actor: actor
|
|
284
|
+
actor: actor
|
|
183
285
|
)
|
|
184
286
|
|
|
185
287
|
# Validate reason is provided
|
|
@@ -193,7 +295,18 @@ module PgSqlTriggers
|
|
|
193
295
|
drop_existing_trigger_for_re_execute
|
|
194
296
|
recreate_trigger
|
|
195
297
|
update_registry_after_re_execute
|
|
298
|
+
after_state = capture_state
|
|
299
|
+
diff = drift_info ? "#{drift_info[:expected_sql]} -> #{after_state[:function_body]}" : nil
|
|
300
|
+
log_audit_success(:trigger_re_execute, actor, reason: reason, confirmation_text: confirmation,
|
|
301
|
+
before_state: before_state, after_state: after_state, diff: diff)
|
|
196
302
|
end
|
|
303
|
+
rescue StandardError => e
|
|
304
|
+
log_audit_failure(
|
|
305
|
+
:trigger_re_execute, actor, e.message, reason: reason,
|
|
306
|
+
confirmation_text: confirmation,
|
|
307
|
+
before_state: before_state
|
|
308
|
+
)
|
|
309
|
+
raise
|
|
197
310
|
end
|
|
198
311
|
|
|
199
312
|
private
|
|
@@ -282,9 +395,13 @@ module PgSqlTriggers
|
|
|
282
395
|
|
|
283
396
|
def recreate_trigger
|
|
284
397
|
ActiveRecord::Base.connection.execute(function_body)
|
|
285
|
-
|
|
398
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
399
|
+
Rails.logger.info "[TRIGGER_RE_EXECUTE] Re-created trigger"
|
|
400
|
+
end
|
|
286
401
|
rescue ActiveRecord::StatementInvalid, StandardError => e
|
|
287
|
-
Rails.
|
|
402
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
403
|
+
Rails.logger.error("[TRIGGER_RE_EXECUTE] Failed: #{e.message}")
|
|
404
|
+
end
|
|
288
405
|
raise
|
|
289
406
|
end
|
|
290
407
|
|
|
@@ -292,6 +409,58 @@ module PgSqlTriggers
|
|
|
292
409
|
update!(enabled: true, last_executed_at: Time.current)
|
|
293
410
|
Rails.logger.info "[TRIGGER_RE_EXECUTE] Updated registry" if defined?(Rails.logger)
|
|
294
411
|
end
|
|
412
|
+
|
|
413
|
+
# Audit logging helpers
|
|
414
|
+
def capture_state
|
|
415
|
+
{
|
|
416
|
+
enabled: enabled,
|
|
417
|
+
version: version,
|
|
418
|
+
checksum: checksum,
|
|
419
|
+
table_name: table_name,
|
|
420
|
+
source: source,
|
|
421
|
+
environment: environment,
|
|
422
|
+
installed_at: installed_at&.iso8601
|
|
423
|
+
}
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# rubocop:disable Metrics/ParameterLists
|
|
427
|
+
def log_audit_success(operation, actor, reason: nil, confirmation_text: nil,
|
|
428
|
+
before_state: nil, after_state: nil, diff: nil)
|
|
429
|
+
return unless defined?(PgSqlTriggers::AuditLog)
|
|
430
|
+
|
|
431
|
+
PgSqlTriggers::AuditLog.log_success(
|
|
432
|
+
operation: operation,
|
|
433
|
+
trigger_name: trigger_name,
|
|
434
|
+
actor: actor,
|
|
435
|
+
environment: Rails.env,
|
|
436
|
+
reason: reason,
|
|
437
|
+
confirmation_text: confirmation_text,
|
|
438
|
+
before_state: before_state,
|
|
439
|
+
after_state: after_state,
|
|
440
|
+
diff: diff
|
|
441
|
+
)
|
|
442
|
+
rescue StandardError => e
|
|
443
|
+
Rails.logger.error("Failed to log audit entry: #{e.message}") if defined?(Rails.logger)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def log_audit_failure(operation, actor, error_message, reason: nil,
|
|
447
|
+
confirmation_text: nil, before_state: nil)
|
|
448
|
+
return unless defined?(PgSqlTriggers::AuditLog)
|
|
449
|
+
|
|
450
|
+
PgSqlTriggers::AuditLog.log_failure(
|
|
451
|
+
operation: operation,
|
|
452
|
+
trigger_name: trigger_name,
|
|
453
|
+
actor: actor,
|
|
454
|
+
environment: Rails.env,
|
|
455
|
+
error_message: error_message,
|
|
456
|
+
reason: reason,
|
|
457
|
+
confirmation_text: confirmation_text,
|
|
458
|
+
before_state: before_state
|
|
459
|
+
)
|
|
460
|
+
rescue StandardError => e
|
|
461
|
+
Rails.logger.error("Failed to log audit entry: #{e.message}") if defined?(Rails.logger)
|
|
462
|
+
end
|
|
463
|
+
# rubocop:enable Metrics/ParameterLists
|
|
295
464
|
end
|
|
296
465
|
# rubocop:enable Metrics/ClassLength
|
|
297
466
|
end
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
<div>
|
|
19
19
|
<%= link_to "Dashboard", root_path, style: "color: white; margin-right: 1rem;" %>
|
|
20
20
|
<%= link_to "Tables", tables_path, style: "color: white; margin-right: 1rem;" %>
|
|
21
|
-
<%= link_to "Generator", new_generator_path, style: "color: white;" %>
|
|
21
|
+
<%= link_to "Generator", new_generator_path, style: "color: white; margin-right: 1rem;" %>
|
|
22
|
+
<%= link_to "Audit Log", audit_logs_path, style: "color: white;" %>
|
|
22
23
|
</div>
|
|
23
24
|
</div>
|
|
24
25
|
</nav>
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
<% if flash[:error] %>
|
|
36
37
|
<div style="background: #f8d7da; color: #721c24; padding: 1rem; margin-bottom: 1rem; border-radius: 4px; border-left: 4px solid #dc3545;">
|
|
37
38
|
<% error_message = flash[:error].to_s %>
|
|
38
|
-
<% if error_message.include?("Kill switch is active") %>
|
|
39
|
+
<% if error_message.include?("Kill switch is active") || error_message.include?("KILL_SWITCH") %>
|
|
39
40
|
<%# Format kill switch error messages nicely %>
|
|
40
41
|
<% lines = error_message.split("\n") %>
|
|
41
42
|
|
|
@@ -55,16 +56,35 @@
|
|
|
55
56
|
|
|
56
57
|
<%# Instructions section with preserved formatting %>
|
|
57
58
|
<div style="margin-top: 0.5rem;">
|
|
58
|
-
<%# Extract and format the instructions portion %>
|
|
59
|
-
<%
|
|
60
|
-
<%
|
|
59
|
+
<%# Extract and format the instructions/recovery portion %>
|
|
60
|
+
<% recovery_start = error_message.index("Recovery:") || error_message.index("To override") || 0 %>
|
|
61
|
+
<% recovery_text = error_message[recovery_start..-1] %>
|
|
62
|
+
<% if recovery_text.start_with?("Recovery:") %>
|
|
63
|
+
<% recovery_text = recovery_text.sub("Recovery:", "").strip %>
|
|
64
|
+
<% end %>
|
|
61
65
|
<div style="white-space: pre-wrap; word-wrap: break-word; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6;">
|
|
62
|
-
<%=
|
|
66
|
+
<%= recovery_text.strip %>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<% elsif error_message.include?("Permission denied") || error_message.include?("PERMISSION_DENIED") %>
|
|
70
|
+
<%# Format permission error messages %>
|
|
71
|
+
<div style="display: flex; align-items: flex-start; margin-bottom: 0.5rem;">
|
|
72
|
+
<span style="font-size: 1.5em; margin-right: 0.75rem;">🔒</span>
|
|
73
|
+
<div style="flex: 1;">
|
|
74
|
+
<div style="font-weight: bold; font-size: 1.1em; margin-bottom: 0.5rem;">Permission Denied</div>
|
|
75
|
+
<div style="white-space: pre-wrap; word-wrap: break-word;"><%= error_message %></div>
|
|
63
76
|
</div>
|
|
64
77
|
</div>
|
|
65
78
|
<% else %>
|
|
66
79
|
<%# Regular error message - preserve formatting %>
|
|
67
80
|
<div style="white-space: pre-wrap; word-wrap: break-word;"><%= error_message %></div>
|
|
81
|
+
<% if Rails.env.development? && defined?(exception) && exception %>
|
|
82
|
+
<%# Show stack trace in development %>
|
|
83
|
+
<details style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #f5c6cb;">
|
|
84
|
+
<summary style="cursor: pointer; font-weight: bold; color: #721c24;">Stack Trace (Development Only)</summary>
|
|
85
|
+
<pre style="background: rgba(0,0,0,0.05); padding: 0.5rem; margin-top: 0.5rem; border-radius: 4px; overflow-x: auto; font-size: 0.85em;"><%= exception.backtrace.first(10).join("\n") %></pre>
|
|
86
|
+
</details>
|
|
87
|
+
<% end %>
|
|
68
88
|
<% end %>
|
|
69
89
|
</div>
|
|
70
90
|
<% end %>
|