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,50 @@
|
|
|
1
|
+
// AJAX handlers for trigger actions
|
|
2
|
+
(function() {
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
// Handle AJAX form submissions
|
|
6
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
7
|
+
// Set up AJAX error handling
|
|
8
|
+
document.addEventListener('ajax:error', function(event) {
|
|
9
|
+
const detail = event.detail || [];
|
|
10
|
+
const error = detail[0] || {};
|
|
11
|
+
const status = error.status || 500;
|
|
12
|
+
const message = error.message || 'An error occurred';
|
|
13
|
+
|
|
14
|
+
alert('Error: ' + message);
|
|
15
|
+
console.error('AJAX Error:', error);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Handle AJAX success for trigger actions
|
|
19
|
+
document.addEventListener('ajax:success', function(event) {
|
|
20
|
+
const [data, status, xhr] = event.detail;
|
|
21
|
+
|
|
22
|
+
// If the response contains a redirect, follow it
|
|
23
|
+
if (xhr.getResponseHeader('Location')) {
|
|
24
|
+
window.location.href = xhr.getResponseHeader('Location');
|
|
25
|
+
} else {
|
|
26
|
+
// Otherwise, reload the page to show updated state
|
|
27
|
+
window.location.reload();
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Handle AJAX complete to show loading states
|
|
32
|
+
document.addEventListener('ajax:before', function(event) {
|
|
33
|
+
const form = event.target;
|
|
34
|
+
const submitButton = form.querySelector('button[type="submit"], button[type="button"]');
|
|
35
|
+
if (submitButton) {
|
|
36
|
+
submitButton.disabled = true;
|
|
37
|
+
submitButton.textContent = 'Processing...';
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
document.addEventListener('ajax:complete', function(event) {
|
|
42
|
+
const form = event.target;
|
|
43
|
+
const submitButton = form.querySelector('button[type="submit"], button[type="button"]');
|
|
44
|
+
if (submitButton) {
|
|
45
|
+
submitButton.disabled = false;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
})();
|
|
50
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgSqlTriggers
|
|
4
|
+
module ErrorHandling
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
# Handles errors and formats them for display.
|
|
8
|
+
# Returns a formatted error message for flash display.
|
|
9
|
+
#
|
|
10
|
+
# @param error [Exception] The error to format
|
|
11
|
+
# @return [String] Formatted error message
|
|
12
|
+
def format_error_for_flash(error)
|
|
13
|
+
return error.to_s unless error.is_a?(PgSqlTriggers::Error)
|
|
14
|
+
|
|
15
|
+
# Use user_message which includes recovery suggestions
|
|
16
|
+
error.user_message
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Rescues from PgSqlTriggers errors and sets appropriate flash messages.
|
|
20
|
+
#
|
|
21
|
+
# @param error [Exception] The error to handle
|
|
22
|
+
# @return [void]
|
|
23
|
+
def rescue_pg_sql_triggers_error(error)
|
|
24
|
+
Rails.logger.error("#{error.class.name}: #{error.message}")
|
|
25
|
+
Rails.logger.error(error.backtrace.join("\n")) if Rails.env.development? && error.respond_to?(:backtrace)
|
|
26
|
+
|
|
27
|
+
flash[:error] = if error.is_a?(PgSqlTriggers::Error)
|
|
28
|
+
format_error_for_flash(error)
|
|
29
|
+
else
|
|
30
|
+
"An unexpected error occurred: #{error.message}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Handles kill switch errors with appropriate flash message and redirect.
|
|
35
|
+
#
|
|
36
|
+
# @param error [PgSqlTriggers::KillSwitchError] The kill switch error
|
|
37
|
+
# @param redirect_path [String, nil] Optional redirect path (defaults to root_path)
|
|
38
|
+
# @return [void]
|
|
39
|
+
def handle_kill_switch_error(error, redirect_path: nil)
|
|
40
|
+
flash[:error] = error.message
|
|
41
|
+
redirect_to redirect_path || root_path
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Handles standard errors with logging and flash message.
|
|
45
|
+
#
|
|
46
|
+
# @param error [Exception] The error to handle
|
|
47
|
+
# @param operation [String] Description of the operation that failed
|
|
48
|
+
# @param redirect_path [String, nil] Optional redirect path (defaults to root_path)
|
|
49
|
+
# @return [void]
|
|
50
|
+
def handle_standard_error(error, operation:, redirect_path: nil)
|
|
51
|
+
Rails.logger.error("#{operation} failed: #{error.message}\n#{error.backtrace.join("\n")}")
|
|
52
|
+
flash[:error] = "#{operation}: #{error.message}"
|
|
53
|
+
redirect_to redirect_path || root_path
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgSqlTriggers
|
|
4
|
+
module KillSwitchProtection
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
# Helper method available in views
|
|
9
|
+
helper_method :kill_switch_active?, :expected_confirmation_text, :current_environment
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Checks if kill switch is active for the current environment.
|
|
13
|
+
#
|
|
14
|
+
# @return [Boolean] true if kill switch is active, false otherwise
|
|
15
|
+
def kill_switch_active?
|
|
16
|
+
PgSqlTriggers::SQL::KillSwitch.active?(environment: current_environment)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Checks kill switch before executing a dangerous operation.
|
|
20
|
+
# Raises KillSwitchError if the operation is blocked.
|
|
21
|
+
#
|
|
22
|
+
# @param operation [Symbol] The operation being performed
|
|
23
|
+
# @param confirmation [String, nil] Optional confirmation text from params
|
|
24
|
+
# @raise [PgSqlTriggers::KillSwitchError] If the operation is blocked
|
|
25
|
+
# @return [true] If the operation is allowed
|
|
26
|
+
def check_kill_switch(operation:, confirmation: nil)
|
|
27
|
+
PgSqlTriggers::SQL::KillSwitch.check!(
|
|
28
|
+
operation: operation,
|
|
29
|
+
environment: current_environment,
|
|
30
|
+
confirmation: confirmation,
|
|
31
|
+
actor: current_actor
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Before action to require kill switch override for an action.
|
|
36
|
+
# Add to specific controller actions that need protection:
|
|
37
|
+
# before_action -> { require_kill_switch_override(:operation_name) }, only: [:dangerous_action]
|
|
38
|
+
#
|
|
39
|
+
# @param operation [Symbol] The operation name
|
|
40
|
+
# @param confirmation [String, nil] Optional confirmation text
|
|
41
|
+
# @raise [PgSqlTriggers::KillSwitchError] If the operation is blocked
|
|
42
|
+
def require_kill_switch_override(operation, confirmation: nil)
|
|
43
|
+
check_kill_switch(operation: operation, confirmation: confirmation)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns the expected confirmation text for an operation (for use in views).
|
|
47
|
+
#
|
|
48
|
+
# @param operation [Symbol] The operation name
|
|
49
|
+
# @return [String] The expected confirmation text
|
|
50
|
+
def expected_confirmation_text(operation)
|
|
51
|
+
if PgSqlTriggers.respond_to?(:kill_switch_confirmation_pattern) &&
|
|
52
|
+
PgSqlTriggers.kill_switch_confirmation_pattern.respond_to?(:call)
|
|
53
|
+
PgSqlTriggers.kill_switch_confirmation_pattern.call(operation)
|
|
54
|
+
else
|
|
55
|
+
"EXECUTE #{operation.to_s.upcase}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns the current environment.
|
|
60
|
+
#
|
|
61
|
+
# @return [String] The current Rails environment
|
|
62
|
+
def current_environment
|
|
63
|
+
Rails.env
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgSqlTriggers
|
|
4
|
+
module PermissionChecking
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
# Helper methods available in views
|
|
9
|
+
helper_method :current_actor, :can_view_triggers?, :can_enable_disable_triggers?,
|
|
10
|
+
:can_drop_triggers?, :can_execute_sql?, :can_generate_triggers?, :can_apply_triggers?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Returns the current actor (user) performing the action.
|
|
14
|
+
# Override this method in host application to provide actual user.
|
|
15
|
+
#
|
|
16
|
+
# @return [Hash] Actor information with :type and :id keys
|
|
17
|
+
def current_actor
|
|
18
|
+
{
|
|
19
|
+
type: current_user_type,
|
|
20
|
+
id: current_user_id
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Returns the current user type.
|
|
25
|
+
# Override this method in host application.
|
|
26
|
+
#
|
|
27
|
+
# @return [String] User type (default: "User")
|
|
28
|
+
def current_user_type
|
|
29
|
+
"User"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns the current user ID.
|
|
33
|
+
# Override this method in host application.
|
|
34
|
+
#
|
|
35
|
+
# @return [String] User ID (default: "unknown")
|
|
36
|
+
def current_user_id
|
|
37
|
+
"unknown"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Checks if current actor has viewer permissions.
|
|
41
|
+
#
|
|
42
|
+
# @raise [ActionController::RedirectError] Redirects if permission denied
|
|
43
|
+
def check_viewer_permission
|
|
44
|
+
can_access = begin
|
|
45
|
+
PgSqlTriggers::Permissions.can?(current_actor, :view_triggers, environment: current_environment)
|
|
46
|
+
rescue StandardError => e
|
|
47
|
+
Rails.logger.error("Permission check failed: #{e.message}\n#{e.backtrace.join("\n")}")
|
|
48
|
+
false
|
|
49
|
+
end
|
|
50
|
+
return if can_access
|
|
51
|
+
|
|
52
|
+
redirect_to root_path, alert: "Insufficient permissions. Viewer role required."
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Checks if current actor has operator permissions (enable/disable/apply).
|
|
56
|
+
#
|
|
57
|
+
# @raise [ActionController::RedirectError] Redirects if permission denied
|
|
58
|
+
def check_operator_permission
|
|
59
|
+
can_access = begin
|
|
60
|
+
PgSqlTriggers::Permissions.can?(current_actor, :enable_trigger, environment: current_environment)
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
Rails.logger.error("Permission check failed: #{e.message}\n#{e.backtrace.join("\n")}")
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
return if can_access
|
|
66
|
+
|
|
67
|
+
redirect_to root_path, alert: "Insufficient permissions. Operator role required."
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Checks if current actor has admin permissions (drop/re-execute/execute SQL).
|
|
71
|
+
#
|
|
72
|
+
# @raise [ActionController::RedirectError] Redirects if permission denied
|
|
73
|
+
def check_admin_permission
|
|
74
|
+
can_access = begin
|
|
75
|
+
PgSqlTriggers::Permissions.can?(current_actor, :drop_trigger, environment: current_environment)
|
|
76
|
+
rescue StandardError => e
|
|
77
|
+
Rails.logger.error("Permission check failed: #{e.message}\n#{e.backtrace.join("\n")}")
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
return if can_access
|
|
81
|
+
|
|
82
|
+
redirect_to root_path, alert: "Insufficient permissions. Admin role required."
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Permission helper methods for views
|
|
86
|
+
|
|
87
|
+
# @return [Boolean] true if current actor can view triggers
|
|
88
|
+
def can_view_triggers?
|
|
89
|
+
PgSqlTriggers::Permissions.can?(current_actor, :view_triggers, environment: current_environment)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return [Boolean] true if current actor can enable/disable triggers
|
|
93
|
+
def can_enable_disable_triggers?
|
|
94
|
+
PgSqlTriggers::Permissions.can?(current_actor, :enable_trigger, environment: current_environment)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @return [Boolean] true if current actor can drop triggers
|
|
98
|
+
def can_drop_triggers?
|
|
99
|
+
PgSqlTriggers::Permissions.can?(current_actor, :drop_trigger, environment: current_environment)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @return [Boolean] true if current actor can execute SQL capsules
|
|
103
|
+
def can_execute_sql?
|
|
104
|
+
PgSqlTriggers::Permissions.can?(current_actor, :execute_sql, environment: current_environment)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @return [Boolean] true if current actor can generate triggers
|
|
108
|
+
def can_generate_triggers?
|
|
109
|
+
PgSqlTriggers::Permissions.can?(current_actor, :apply_trigger, environment: current_environment)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @return [Boolean] true if current actor can apply triggers
|
|
113
|
+
def can_apply_triggers?
|
|
114
|
+
PgSqlTriggers::Permissions.can?(current_actor, :apply_trigger, environment: current_environment)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -1,81 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PgSqlTriggers
|
|
4
|
+
# Base controller for all pg_sql_triggers controllers.
|
|
5
|
+
# Includes common concerns for kill switch protection, permission checking, and error handling.
|
|
4
6
|
class ApplicationController < ActionController::Base
|
|
5
7
|
include PgSqlTriggers::Engine.routes.url_helpers
|
|
8
|
+
include PgSqlTriggers::KillSwitchProtection
|
|
9
|
+
include PgSqlTriggers::PermissionChecking
|
|
10
|
+
include PgSqlTriggers::ErrorHandling
|
|
6
11
|
|
|
7
12
|
protect_from_forgery with: :exception
|
|
8
13
|
layout "pg_sql_triggers/application"
|
|
9
14
|
|
|
10
15
|
before_action :check_permissions?
|
|
11
16
|
|
|
12
|
-
#
|
|
13
|
-
|
|
17
|
+
# Include permissions helper for view helpers
|
|
18
|
+
include PgSqlTriggers::PermissionsHelper
|
|
14
19
|
|
|
15
20
|
private
|
|
16
21
|
|
|
22
|
+
# Override this method in host application to implement custom permission checks.
|
|
23
|
+
#
|
|
24
|
+
# @return [Boolean] true if permissions check passes
|
|
17
25
|
def check_permissions?
|
|
18
|
-
# Override this method in host application to implement custom permission checks
|
|
19
26
|
true
|
|
20
27
|
end
|
|
21
|
-
|
|
22
|
-
def current_actor
|
|
23
|
-
# Override this in host application to provide actual user
|
|
24
|
-
{
|
|
25
|
-
type: current_user_type,
|
|
26
|
-
id: current_user_id
|
|
27
|
-
}
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def current_user_type
|
|
31
|
-
"User"
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def current_user_id
|
|
35
|
-
"unknown"
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# ========== Kill Switch Helpers ==========
|
|
39
|
-
|
|
40
|
-
# Returns the current environment
|
|
41
|
-
def current_environment
|
|
42
|
-
Rails.env
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Checks if kill switch is active for the current environment
|
|
46
|
-
def kill_switch_active?
|
|
47
|
-
PgSqlTriggers::SQL::KillSwitch.active?(environment: current_environment)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Checks kill switch before executing a dangerous operation
|
|
51
|
-
# Raises KillSwitchError if the operation is blocked
|
|
52
|
-
#
|
|
53
|
-
# @param operation [Symbol] The operation being performed
|
|
54
|
-
# @param confirmation [String, nil] Optional confirmation text from params
|
|
55
|
-
def check_kill_switch(operation:, confirmation: nil)
|
|
56
|
-
PgSqlTriggers::SQL::KillSwitch.check!(
|
|
57
|
-
operation: operation,
|
|
58
|
-
environment: current_environment,
|
|
59
|
-
confirmation: confirmation,
|
|
60
|
-
actor: current_actor
|
|
61
|
-
)
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# Before action to require kill switch override for an action
|
|
65
|
-
# Add to specific controller actions that need protection:
|
|
66
|
-
# before_action -> { require_kill_switch_override(:operation_name) }, only: [:dangerous_action]
|
|
67
|
-
def require_kill_switch_override(operation, confirmation: nil)
|
|
68
|
-
check_kill_switch(operation: operation, confirmation: confirmation)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Returns the expected confirmation text for an operation (for use in views)
|
|
72
|
-
def expected_confirmation_text(operation)
|
|
73
|
-
if PgSqlTriggers.respond_to?(:kill_switch_confirmation_pattern) &&
|
|
74
|
-
PgSqlTriggers.kill_switch_confirmation_pattern.respond_to?(:call)
|
|
75
|
-
PgSqlTriggers.kill_switch_confirmation_pattern.call(operation)
|
|
76
|
-
else
|
|
77
|
-
"EXECUTE #{operation.to_s.upcase}"
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
28
|
end
|
|
81
29
|
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgSqlTriggers
|
|
4
|
+
class AuditLogsController < ApplicationController
|
|
5
|
+
before_action :check_viewer_permission
|
|
6
|
+
|
|
7
|
+
# GET /audit_logs
|
|
8
|
+
# Display audit log entries with filtering and sorting
|
|
9
|
+
def index
|
|
10
|
+
@audit_logs = PgSqlTriggers::AuditLog.all
|
|
11
|
+
|
|
12
|
+
# Filter by trigger name
|
|
13
|
+
@audit_logs = @audit_logs.for_trigger(params[:trigger_name]) if params[:trigger_name].present?
|
|
14
|
+
|
|
15
|
+
# Filter by operation
|
|
16
|
+
@audit_logs = @audit_logs.for_operation(params[:operation]) if params[:operation].present?
|
|
17
|
+
|
|
18
|
+
# Filter by status
|
|
19
|
+
if params[:status].present? && %w[success failure].include?(params[:status])
|
|
20
|
+
@audit_logs = @audit_logs.where(status: params[:status])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Filter by environment
|
|
24
|
+
@audit_logs = @audit_logs.for_environment(params[:environment]) if params[:environment].present?
|
|
25
|
+
|
|
26
|
+
# Filter by actor (search in JSONB field)
|
|
27
|
+
@audit_logs = @audit_logs.where("actor->>'id' = ?", params[:actor_id]) if params[:actor_id].present?
|
|
28
|
+
|
|
29
|
+
# Sort by date (default: most recent first)
|
|
30
|
+
sort_direction = params[:sort] == "asc" ? :asc : :desc
|
|
31
|
+
@audit_logs = @audit_logs.order(created_at: sort_direction)
|
|
32
|
+
|
|
33
|
+
# Pagination
|
|
34
|
+
@per_page = (params[:per_page] || 50).to_i
|
|
35
|
+
@per_page = [@per_page, 200].min # Cap at 200
|
|
36
|
+
@page = (params[:page] || 1).to_i
|
|
37
|
+
@total_count = @audit_logs.count
|
|
38
|
+
@total_pages = @total_count.positive? ? (@total_count.to_f / @per_page).ceil : 1
|
|
39
|
+
@page = @page.clamp(1, @total_pages)
|
|
40
|
+
|
|
41
|
+
offset = (@page - 1) * @per_page
|
|
42
|
+
@audit_logs = @audit_logs.offset(offset).limit(@per_page)
|
|
43
|
+
|
|
44
|
+
# Get distinct values for filter dropdowns
|
|
45
|
+
@available_trigger_names = PgSqlTriggers::AuditLog.distinct.pluck(:trigger_name).compact.sort
|
|
46
|
+
@available_operations = PgSqlTriggers::AuditLog.distinct.pluck(:operation).compact.sort
|
|
47
|
+
@available_environments = PgSqlTriggers::AuditLog.distinct.pluck(:environment).compact.sort
|
|
48
|
+
|
|
49
|
+
respond_to do |format|
|
|
50
|
+
format.html
|
|
51
|
+
format.csv do
|
|
52
|
+
send_data generate_csv, filename: "audit_logs_#{Time.current.strftime('%Y%m%d_%H%M%S')}.csv",
|
|
53
|
+
type: "text/csv", disposition: "attachment"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def generate_csv
|
|
61
|
+
require "csv"
|
|
62
|
+
|
|
63
|
+
# Get all audit logs (no pagination for CSV)
|
|
64
|
+
audit_logs = PgSqlTriggers::AuditLog.all
|
|
65
|
+
|
|
66
|
+
# Apply filters
|
|
67
|
+
audit_logs = audit_logs.for_trigger(params[:trigger_name]) if params[:trigger_name].present?
|
|
68
|
+
audit_logs = audit_logs.for_operation(params[:operation]) if params[:operation].present?
|
|
69
|
+
if params[:status].present? && %w[success failure].include?(params[:status])
|
|
70
|
+
audit_logs = audit_logs.where(status: params[:status])
|
|
71
|
+
end
|
|
72
|
+
audit_logs = audit_logs.for_environment(params[:environment]) if params[:environment].present?
|
|
73
|
+
audit_logs = audit_logs.where("actor->>'id' = ?", params[:actor_id]) if params[:actor_id].present?
|
|
74
|
+
|
|
75
|
+
CSV.generate(headers: true) do |csv|
|
|
76
|
+
csv << [
|
|
77
|
+
"ID", "Trigger Name", "Operation", "Status", "Environment",
|
|
78
|
+
"Actor Type", "Actor ID", "Reason", "Error Message",
|
|
79
|
+
"Created At"
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
audit_logs.order(created_at: :desc).find_each do |log|
|
|
83
|
+
actor_type = log.actor.is_a?(Hash) ? log.actor["type"] || log.actor[:type] : nil
|
|
84
|
+
actor_id = log.actor.is_a?(Hash) ? log.actor["id"] || log.actor[:id] : nil
|
|
85
|
+
|
|
86
|
+
csv << [
|
|
87
|
+
log.id,
|
|
88
|
+
log.trigger_name || "",
|
|
89
|
+
log.operation,
|
|
90
|
+
log.status,
|
|
91
|
+
log.environment || "",
|
|
92
|
+
actor_type || "",
|
|
93
|
+
actor_id || "",
|
|
94
|
+
log.reason || "",
|
|
95
|
+
log.error_message || "",
|
|
96
|
+
log.created_at&.iso8601 || ""
|
|
97
|
+
]
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -5,7 +5,10 @@ module PgSqlTriggers
|
|
|
5
5
|
before_action :check_viewer_permission
|
|
6
6
|
|
|
7
7
|
def index
|
|
8
|
-
|
|
8
|
+
# Sort by installed_at descending (most recent first), fallback to created_at
|
|
9
|
+
@triggers = PgSqlTriggers::TriggerRegistry.order(
|
|
10
|
+
Arel.sql("COALESCE(installed_at, created_at) DESC")
|
|
11
|
+
)
|
|
9
12
|
|
|
10
13
|
# Get drift summary
|
|
11
14
|
drift_summary = PgSqlTriggers::Drift::Reporter.summary
|
|
@@ -43,13 +46,5 @@ module PgSqlTriggers
|
|
|
43
46
|
@per_page = 20
|
|
44
47
|
end
|
|
45
48
|
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
def check_viewer_permission
|
|
50
|
-
return if PgSqlTriggers::Permissions.can?(current_actor, :view_triggers)
|
|
51
|
-
|
|
52
|
-
redirect_to root_path, alert: "Insufficient permissions. Viewer role required."
|
|
53
|
-
end
|
|
54
49
|
end
|
|
55
50
|
end
|
|
@@ -2,12 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
module PgSqlTriggers
|
|
4
4
|
class TablesController < ApplicationController
|
|
5
|
+
before_action :check_viewer_permission
|
|
6
|
+
|
|
5
7
|
def index
|
|
6
8
|
all_tables = PgSqlTriggers::DatabaseIntrospection.new.tables_with_triggers
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@
|
|
10
|
-
@
|
|
9
|
+
|
|
10
|
+
# Calculate statistics
|
|
11
|
+
@tables_with_trigger_count = all_tables.count { |t| t[:trigger_count].positive? }
|
|
12
|
+
@tables_without_trigger_count = all_tables.count { |t| t[:trigger_count].zero? }
|
|
13
|
+
@total_tables_count = all_tables.count
|
|
14
|
+
|
|
15
|
+
# Filter based on parameter
|
|
16
|
+
@filter = params[:filter] || "with_triggers"
|
|
17
|
+
filtered_tables = case @filter
|
|
18
|
+
when "with_triggers"
|
|
19
|
+
all_tables.select { |t| t[:trigger_count].positive? }
|
|
20
|
+
when "without_triggers"
|
|
21
|
+
all_tables.select { |t| t[:trigger_count].zero? }
|
|
22
|
+
else # 'all'
|
|
23
|
+
all_tables
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
@total_tables = filtered_tables.count
|
|
27
|
+
|
|
28
|
+
# Pagination
|
|
29
|
+
@per_page = (params[:per_page] || 20).to_i
|
|
30
|
+
@per_page = [@per_page, 100].min # Cap at 100
|
|
31
|
+
@page = (params[:page] || 1).to_i
|
|
32
|
+
@total_pages = @total_tables.positive? ? (@total_tables.to_f / @per_page).ceil : 1
|
|
33
|
+
@page = @page.clamp(1, @total_pages) # Ensure page is within valid range
|
|
34
|
+
|
|
35
|
+
offset = (@page - 1) * @per_page
|
|
36
|
+
@tables_with_triggers = filtered_tables.slice(offset, @per_page) || []
|
|
11
37
|
end
|
|
12
38
|
|
|
13
39
|
def show
|
|
@@ -4,10 +4,10 @@ module PgSqlTriggers
|
|
|
4
4
|
# Controller for managing individual triggers via web UI
|
|
5
5
|
# Provides actions to enable and disable triggers
|
|
6
6
|
class TriggersController < ApplicationController
|
|
7
|
-
before_action :set_trigger, only: %i[show enable disable drop re_execute]
|
|
8
7
|
before_action :check_viewer_permission, only: [:show]
|
|
9
8
|
before_action :check_operator_permission, only: %i[enable disable]
|
|
10
9
|
before_action :check_admin_permission, only: %i[drop re_execute]
|
|
10
|
+
before_action :set_trigger, only: %i[show enable disable drop re_execute]
|
|
11
11
|
|
|
12
12
|
def show
|
|
13
13
|
# Load trigger details and drift information
|
|
@@ -18,7 +18,7 @@ module PgSqlTriggers
|
|
|
18
18
|
# Check kill switch before enabling trigger
|
|
19
19
|
check_kill_switch(operation: :ui_trigger_enable, confirmation: params[:confirmation_text])
|
|
20
20
|
|
|
21
|
-
@trigger.enable!(confirmation: params[:confirmation_text])
|
|
21
|
+
@trigger.enable!(confirmation: params[:confirmation_text], actor: current_actor)
|
|
22
22
|
flash[:success] = "Trigger '#{@trigger.trigger_name}' enabled successfully."
|
|
23
23
|
redirect_to redirect_path
|
|
24
24
|
rescue PgSqlTriggers::KillSwitchError => e
|
|
@@ -34,7 +34,7 @@ module PgSqlTriggers
|
|
|
34
34
|
# Check kill switch before disabling trigger
|
|
35
35
|
check_kill_switch(operation: :ui_trigger_disable, confirmation: params[:confirmation_text])
|
|
36
36
|
|
|
37
|
-
@trigger.disable!(confirmation: params[:confirmation_text])
|
|
37
|
+
@trigger.disable!(confirmation: params[:confirmation_text], actor: current_actor)
|
|
38
38
|
flash[:success] = "Trigger '#{@trigger.trigger_name}' disabled successfully."
|
|
39
39
|
redirect_to redirect_path
|
|
40
40
|
rescue PgSqlTriggers::KillSwitchError => e
|
|
@@ -119,24 +119,6 @@ module PgSqlTriggers
|
|
|
119
119
|
redirect_to root_path
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
-
def check_viewer_permission
|
|
123
|
-
return if PgSqlTriggers::Permissions.can?(current_actor, :view_triggers)
|
|
124
|
-
|
|
125
|
-
redirect_to root_path, alert: "Insufficient permissions. Viewer role required."
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def check_operator_permission
|
|
129
|
-
return if PgSqlTriggers::Permissions.can?(current_actor, :enable_trigger)
|
|
130
|
-
|
|
131
|
-
redirect_to root_path, alert: "Insufficient permissions. Operator role required."
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def check_admin_permission
|
|
135
|
-
return if PgSqlTriggers::Permissions.can?(current_actor, :drop_trigger)
|
|
136
|
-
|
|
137
|
-
redirect_to root_path, alert: "Insufficient permissions. Admin role required."
|
|
138
|
-
end
|
|
139
|
-
|
|
140
122
|
def redirect_path
|
|
141
123
|
# Redirect back to the referring page if possible, otherwise to dashboard
|
|
142
124
|
params[:redirect_to].presence || request.referer || root_path
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgSqlTriggers
|
|
4
|
+
module PermissionsHelper
|
|
5
|
+
# Check if the current actor can perform an action
|
|
6
|
+
#
|
|
7
|
+
# @param action [Symbol, String] The action to check
|
|
8
|
+
# @return [Boolean] True if the actor can perform the action
|
|
9
|
+
def can?(action)
|
|
10
|
+
PgSqlTriggers::Permissions.can?(current_actor, action, environment: current_environment)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Check if the current actor can view triggers
|
|
14
|
+
def can_view_triggers?
|
|
15
|
+
can?(:view_triggers)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Check if the current actor can enable/disable triggers
|
|
19
|
+
def can_enable_disable_triggers?
|
|
20
|
+
can?(:enable_trigger)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Check if the current actor can drop triggers
|
|
24
|
+
def can_drop_triggers?
|
|
25
|
+
can?(:drop_trigger)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Check if the current actor can execute SQL capsules
|
|
29
|
+
def can_execute_sql?
|
|
30
|
+
can?(:execute_sql)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Check if the current actor can generate triggers
|
|
34
|
+
def can_generate_triggers?
|
|
35
|
+
can?(:generate_trigger)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Check if the current actor can apply triggers (run migrations)
|
|
39
|
+
def can_apply_triggers?
|
|
40
|
+
can?(:apply_trigger)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|