dbviewer 0.6.2 → 0.6.3
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/README.md +1 -1
- data/app/controllers/concerns/dbviewer/database_operations.rb +89 -100
- data/app/controllers/dbviewer/api/entity_relationship_diagrams_controller.rb +25 -23
- data/app/controllers/dbviewer/tables_controller.rb +9 -9
- data/app/helpers/dbviewer/application_helper.rb +33 -13
- data/app/views/dbviewer/tables/show.html.erb +225 -139
- data/app/views/layouts/dbviewer/application.html.erb +55 -0
- data/lib/dbviewer/database/dynamic_model_factory.rb +40 -5
- data/lib/dbviewer/datatable/query_operations.rb +52 -197
- data/lib/dbviewer/engine.rb +1 -22
- data/lib/dbviewer/query/executor.rb +1 -1
- data/lib/dbviewer/query/notification_subscriber.rb +46 -0
- data/lib/dbviewer/validator/sql.rb +198 -0
- data/lib/dbviewer/validator.rb +9 -0
- data/lib/dbviewer/version.rb +1 -1
- data/lib/dbviewer.rb +69 -45
- data/lib/generators/dbviewer/templates/initializer.rb +15 -0
- metadata +5 -3
- data/lib/dbviewer/sql_validator.rb +0 -194
data/lib/dbviewer.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
require "dbviewer/version"
|
2
2
|
require "dbviewer/configuration"
|
3
3
|
require "dbviewer/engine"
|
4
|
-
require "dbviewer/
|
4
|
+
require "dbviewer/validator"
|
5
|
+
require "dbviewer/validator/sql"
|
5
6
|
|
6
7
|
require "dbviewer/storage/base"
|
7
8
|
require "dbviewer/storage/in_memory_storage"
|
@@ -11,6 +12,7 @@ require "dbviewer/query/executor"
|
|
11
12
|
require "dbviewer/query/analyzer"
|
12
13
|
require "dbviewer/query/collection"
|
13
14
|
require "dbviewer/query/logger"
|
15
|
+
require "dbviewer/query/notification_subscriber"
|
14
16
|
require "dbviewer/query/parser"
|
15
17
|
|
16
18
|
require "dbviewer/database/cache_manager"
|
@@ -51,61 +53,83 @@ module Dbviewer
|
|
51
53
|
|
52
54
|
# This class method will be called by the engine when it's appropriate
|
53
55
|
def setup
|
54
|
-
|
56
|
+
configure_query_logger
|
57
|
+
validate_database_connections
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Configure the query logger with current configuration settings
|
63
|
+
def configure_query_logger
|
55
64
|
Dbviewer::Query::Logger.configure(
|
56
65
|
enable_query_logging: configuration.enable_query_logging,
|
57
66
|
query_logging_mode: configuration.query_logging_mode
|
58
67
|
)
|
68
|
+
end
|
59
69
|
|
60
|
-
|
61
|
-
|
62
|
-
configuration.database_connections.
|
63
|
-
|
64
|
-
connection_class = nil
|
65
|
-
|
66
|
-
if config[:connection]
|
67
|
-
connection_class = config[:connection]
|
68
|
-
elsif config[:connection_class].is_a?(String)
|
69
|
-
# Try to load the class if it's defined as a string
|
70
|
-
begin
|
71
|
-
connection_class = config[:connection_class].constantize
|
72
|
-
rescue NameError => e
|
73
|
-
Rails.logger.warn "DBViewer could not load connection class #{config[:connection_class]}: #{e.message}"
|
74
|
-
next
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
if connection_class
|
79
|
-
connection = connection_class.connection
|
80
|
-
adapter_name = connection.adapter_name
|
81
|
-
Rails.logger.info "DBViewer successfully connected to #{config[:name] || key.to_s} database (#{adapter_name})"
|
82
|
-
|
83
|
-
# Store the resolved connection class back in the config
|
84
|
-
config[:connection] = connection_class
|
85
|
-
else
|
86
|
-
raise "No valid connection configuration found for #{key}"
|
87
|
-
end
|
88
|
-
rescue => e
|
89
|
-
connection_errors << { key: key, error: e.message }
|
90
|
-
Rails.logger.error "DBViewer could not connect to #{config[:name] || key.to_s} database: #{e.message}"
|
91
|
-
end
|
70
|
+
# Validate all configured database connections
|
71
|
+
def validate_database_connections
|
72
|
+
connection_errors = configuration.database_connections.filter_map do |key, config|
|
73
|
+
validate_single_connection(key, config)
|
92
74
|
end
|
93
75
|
|
94
|
-
|
95
|
-
|
96
|
-
|
76
|
+
raise_if_all_connections_failed(connection_errors)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Validate a single database connection
|
80
|
+
# @param key [Symbol] The connection key
|
81
|
+
# @param config [Hash] The connection configuration
|
82
|
+
# @return [Hash, nil] Error hash if validation failed, nil if successful
|
83
|
+
def validate_single_connection(key, config)
|
84
|
+
connection_class = resolve_connection_class(config)
|
85
|
+
return { key: key, error: "No valid connection configuration found for #{key}" } unless connection_class
|
86
|
+
|
87
|
+
test_connection(connection_class, config, key)
|
88
|
+
store_resolved_connection(config, connection_class)
|
89
|
+
nil
|
90
|
+
rescue => e
|
91
|
+
Rails.logger.error "DBViewer could not connect to #{config[:name] || key.to_s} database: #{e.message}"
|
92
|
+
{ key: key, error: e.message }
|
97
93
|
end
|
98
94
|
|
99
|
-
#
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
95
|
+
# Resolve the connection class from configuration
|
96
|
+
# @param config [Hash] The connection configuration
|
97
|
+
# @return [Class, nil] The resolved connection class or nil
|
98
|
+
def resolve_connection_class(config)
|
99
|
+
return config[:connection] if config[:connection]
|
100
|
+
return nil unless config[:connection_class].is_a?(String)
|
101
|
+
|
102
|
+
begin
|
103
|
+
config[:connection_class].constantize
|
104
|
+
rescue NameError => e
|
105
|
+
Rails.logger.warn "DBViewer could not load connection class #{config[:connection_class]}: #{e.message}"
|
106
|
+
nil
|
105
107
|
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Test the database connection
|
111
|
+
# @param connection_class [Class] The connection class
|
112
|
+
# @param config [Hash] The connection configuration
|
113
|
+
# @param key [Symbol] The connection key
|
114
|
+
def test_connection(connection_class, config, key)
|
115
|
+
connection = connection_class.connection
|
116
|
+
adapter_name = connection.adapter_name
|
117
|
+
Rails.logger.info "DBViewer successfully connected to #{config[:name] || key.to_s} database (#{adapter_name})"
|
118
|
+
end
|
119
|
+
|
120
|
+
# Store the resolved connection class back in the config
|
121
|
+
# @param config [Hash] The connection configuration
|
122
|
+
# @param connection_class [Class] The resolved connection class
|
123
|
+
def store_resolved_connection(config, connection_class)
|
124
|
+
config[:connection] = connection_class
|
125
|
+
end
|
106
126
|
|
107
|
-
|
108
|
-
|
127
|
+
# Raise an error if all database connections failed
|
128
|
+
# @param connection_errors [Array] Array of connection error hashes
|
129
|
+
def raise_if_all_connections_failed(connection_errors)
|
130
|
+
if connection_errors.length == configuration.database_connections.length
|
131
|
+
raise "DBViewer could not connect to any configured database"
|
132
|
+
end
|
109
133
|
end
|
110
134
|
end
|
111
135
|
end
|
@@ -18,4 +18,19 @@ Dbviewer.configure do |config|
|
|
18
18
|
# username: "admin",
|
19
19
|
# password: "your_secure_password"
|
20
20
|
# }
|
21
|
+
|
22
|
+
# Multiple database connections configuration
|
23
|
+
# config.database_connections = {
|
24
|
+
# primary: {
|
25
|
+
# connection_class: "ActiveRecord::Base",
|
26
|
+
# name: "Primary Database"
|
27
|
+
# },
|
28
|
+
# secondary: {
|
29
|
+
# connection_class: "SecondaryDatabase",
|
30
|
+
# name: "Blog Database"
|
31
|
+
# }
|
32
|
+
# }
|
33
|
+
|
34
|
+
# Set the default active connection
|
35
|
+
# config.current_connection = :primary
|
21
36
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dbviewer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wailan Tirajoh
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-06-
|
11
|
+
date: 2025-06-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -97,11 +97,13 @@ files:
|
|
97
97
|
- lib/dbviewer/query/collection.rb
|
98
98
|
- lib/dbviewer/query/executor.rb
|
99
99
|
- lib/dbviewer/query/logger.rb
|
100
|
+
- lib/dbviewer/query/notification_subscriber.rb
|
100
101
|
- lib/dbviewer/query/parser.rb
|
101
|
-
- lib/dbviewer/sql_validator.rb
|
102
102
|
- lib/dbviewer/storage/base.rb
|
103
103
|
- lib/dbviewer/storage/file_storage.rb
|
104
104
|
- lib/dbviewer/storage/in_memory_storage.rb
|
105
|
+
- lib/dbviewer/validator.rb
|
106
|
+
- lib/dbviewer/validator/sql.rb
|
105
107
|
- lib/dbviewer/version.rb
|
106
108
|
- lib/generators/dbviewer/install_generator.rb
|
107
109
|
- lib/generators/dbviewer/templates/initializer.rb
|
@@ -1,194 +0,0 @@
|
|
1
|
-
module Dbviewer
|
2
|
-
# SqlValidator class handles SQL query validation and normalization
|
3
|
-
# to ensure queries are safe (read-only) and properly formatted.
|
4
|
-
# This helps prevent potentially destructive SQL operations.
|
5
|
-
class SqlValidator
|
6
|
-
# List of SQL keywords that could modify data or schema
|
7
|
-
FORBIDDEN_KEYWORDS = %w[
|
8
|
-
UPDATE INSERT DELETE DROP ALTER CREATE TRUNCATE REPLACE
|
9
|
-
RENAME GRANT REVOKE LOCK UNLOCK COMMIT ROLLBACK
|
10
|
-
SAVEPOINT INTO CALL EXECUTE EXEC
|
11
|
-
]
|
12
|
-
|
13
|
-
# List of SQL keywords that should only be allowed in specific contexts
|
14
|
-
CONDITIONAL_KEYWORDS = {
|
15
|
-
# JOIN is allowed, but we should check for suspicious patterns
|
16
|
-
"JOIN" => /\bJOIN\b/i,
|
17
|
-
# UNION is allowed, but potential for injection
|
18
|
-
"UNION" => /\bUNION\b/i,
|
19
|
-
# WITH is allowed for CTEs, but need to ensure it's not a data modification
|
20
|
-
"WITH" => /\bWITH\b/i
|
21
|
-
}
|
22
|
-
|
23
|
-
# Maximum allowed query length
|
24
|
-
MAX_QUERY_LENGTH = 10000
|
25
|
-
|
26
|
-
# Determines if a query is safe (read-only)
|
27
|
-
# @param sql [String] The SQL query to validate
|
28
|
-
# @return [Boolean] true if the query is safe, false otherwise
|
29
|
-
def self.safe_query?(sql)
|
30
|
-
return false if sql.blank?
|
31
|
-
|
32
|
-
# Get max query length from configuration if available
|
33
|
-
max_length = respond_to?(:max_query_length) ? max_query_length : MAX_QUERY_LENGTH
|
34
|
-
return false if sql.length > max_length
|
35
|
-
|
36
|
-
normalized_sql = normalize(sql)
|
37
|
-
|
38
|
-
# Case-insensitive check for SELECT at the beginning
|
39
|
-
return false unless normalized_sql =~ /\A\s*SELECT\s+/i
|
40
|
-
|
41
|
-
# Check for forbidden keywords that might be used in subqueries or other SQL constructs
|
42
|
-
FORBIDDEN_KEYWORDS.each do |keyword|
|
43
|
-
# Look for the keyword with word boundaries to avoid false positives
|
44
|
-
return false if normalized_sql =~ /\b#{keyword}\b/i
|
45
|
-
end
|
46
|
-
|
47
|
-
# Check for suspicious patterns that might indicate SQL injection attempts
|
48
|
-
return false if has_suspicious_patterns?(normalized_sql)
|
49
|
-
|
50
|
-
# Check for multiple statements (;) which could allow executing multiple commands
|
51
|
-
statements = normalized_sql.split(";").reject(&:blank?)
|
52
|
-
return false if statements.size > 1
|
53
|
-
|
54
|
-
# Additional specific checks for common SQL injection patterns
|
55
|
-
return false if has_injection_patterns?(normalized_sql)
|
56
|
-
|
57
|
-
true
|
58
|
-
end
|
59
|
-
|
60
|
-
# Check for suspicious patterns in SQL that might indicate an attack
|
61
|
-
# @param sql [String] Normalized SQL query
|
62
|
-
# @return [Boolean] true if suspicious patterns found, false otherwise
|
63
|
-
def self.has_suspicious_patterns?(sql)
|
64
|
-
# Check for SQL comment sequences that might be used to hide malicious code
|
65
|
-
return true if sql =~ /\s+--/ || sql =~ /\/\*/
|
66
|
-
|
67
|
-
# Check for string concatenation which might be used for injection
|
68
|
-
return true if sql =~ /\|\|/ || sql =~ /CONCAT\s*\(/i
|
69
|
-
|
70
|
-
# Check for excessive number of quotes which might indicate injection
|
71
|
-
single_quotes = sql.count("'")
|
72
|
-
double_quotes = sql.count('"')
|
73
|
-
return true if single_quotes > 20 || double_quotes > 20
|
74
|
-
|
75
|
-
# Check for hex/binary data which might hide malicious code
|
76
|
-
return true if sql =~ /0x[0-9a-f]{16,}/i
|
77
|
-
|
78
|
-
false
|
79
|
-
end
|
80
|
-
|
81
|
-
# Check for specific SQL injection patterns
|
82
|
-
# @param sql [String] Normalized SQL query
|
83
|
-
# @return [Boolean] true if injection patterns found, false otherwise
|
84
|
-
def self.has_injection_patterns?(sql)
|
85
|
-
# Check for typical SQL injection test patterns
|
86
|
-
return true if sql =~ /'\s*OR\s*'.*'\s*=\s*'/i
|
87
|
-
return true if sql =~ /'\s*OR\s*1\s*=\s*1/i
|
88
|
-
return true if sql =~ /'\s*;\s*--/i
|
89
|
-
|
90
|
-
# Check for attempts to determine database type
|
91
|
-
return true if sql =~ /@@version/i
|
92
|
-
return true if sql =~ /version\(\)/i
|
93
|
-
|
94
|
-
false
|
95
|
-
end
|
96
|
-
|
97
|
-
# Normalize SQL by removing comments and extra whitespace
|
98
|
-
# @param sql [String] The SQL query to normalize
|
99
|
-
# @return [String] The normalized SQL query
|
100
|
-
def self.normalize(sql)
|
101
|
-
return "" if sql.nil?
|
102
|
-
|
103
|
-
begin
|
104
|
-
# Remove SQL comments (both -- and /* */ styles)
|
105
|
-
normalized = sql.gsub(/--.*$/, "") # Remove -- style comments
|
106
|
-
.gsub(/\/\*.*?\*\//m, "") # Remove /* */ style comments
|
107
|
-
.gsub(/\s+/, " ") # Normalize whitespace
|
108
|
-
.strip # Remove leading/trailing whitespace
|
109
|
-
|
110
|
-
# Replace multiple spaces with a single space
|
111
|
-
normalized.gsub(/\s{2,}/, " ")
|
112
|
-
rescue => e
|
113
|
-
Rails.logger.error("[DBViewer] SQL normalization error: #{e.message}")
|
114
|
-
""
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
# Validates a query and raises an exception if it's unsafe
|
119
|
-
# @param sql [String] The SQL query to validate
|
120
|
-
# @raise [SecurityError] if the query is unsafe
|
121
|
-
# @return [String] The normalized SQL query if it's safe
|
122
|
-
def self.validate!(sql)
|
123
|
-
if sql.blank?
|
124
|
-
raise SecurityError, "Empty query is not allowed"
|
125
|
-
end
|
126
|
-
|
127
|
-
# Get max query length from configuration if available
|
128
|
-
max_length = respond_to?(:max_query_length) ? max_query_length : MAX_QUERY_LENGTH
|
129
|
-
if sql.length > max_length
|
130
|
-
raise SecurityError, "Query exceeds maximum allowed length (#{max_length} chars)"
|
131
|
-
end
|
132
|
-
|
133
|
-
normalized_sql = normalize(sql)
|
134
|
-
|
135
|
-
# Special case for SQLite PRAGMA statements which are safe read-only commands
|
136
|
-
if normalized_sql =~ /\A\s*PRAGMA\s+[a-z0-9_]+\s*\z/i
|
137
|
-
return normalized_sql
|
138
|
-
end
|
139
|
-
|
140
|
-
unless normalized_sql =~ /\A\s*SELECT\s+/i
|
141
|
-
raise SecurityError, "Query must begin with SELECT for security reasons"
|
142
|
-
end
|
143
|
-
|
144
|
-
FORBIDDEN_KEYWORDS.each do |keyword|
|
145
|
-
if normalized_sql =~ /\b#{keyword}\b/i
|
146
|
-
raise SecurityError, "Forbidden keyword '#{keyword}' detected in query"
|
147
|
-
end
|
148
|
-
end
|
149
|
-
|
150
|
-
if has_suspicious_patterns?(normalized_sql)
|
151
|
-
raise SecurityError, "Query contains suspicious patterns that may indicate SQL injection"
|
152
|
-
end
|
153
|
-
|
154
|
-
# Check for multiple statements
|
155
|
-
statements = normalized_sql.split(";").reject(&:blank?)
|
156
|
-
if statements.size > 1
|
157
|
-
raise SecurityError, "Multiple SQL statements are not allowed"
|
158
|
-
end
|
159
|
-
|
160
|
-
if has_injection_patterns?(normalized_sql)
|
161
|
-
raise SecurityError, "Query contains patterns commonly associated with SQL injection attempts"
|
162
|
-
end
|
163
|
-
|
164
|
-
normalized_sql
|
165
|
-
end
|
166
|
-
|
167
|
-
# Check if a query is using a specific database feature that might need special handling
|
168
|
-
# @param sql [String] The SQL query
|
169
|
-
# @param feature [Symbol] The feature to check for (:join, :subquery, :order_by, etc.)
|
170
|
-
# @return [Boolean] true if the feature is used in the query
|
171
|
-
def self.uses_feature?(sql, feature)
|
172
|
-
normalized = normalize(sql)
|
173
|
-
case feature
|
174
|
-
when :join
|
175
|
-
normalized =~ /\b(INNER|LEFT|RIGHT|FULL|CROSS)?\s*JOIN\b/i
|
176
|
-
when :subquery
|
177
|
-
# Check if there are parentheses that likely contain a subquery
|
178
|
-
normalized.count("(") > normalized.count(")")
|
179
|
-
when :order_by
|
180
|
-
normalized =~ /\bORDER\s+BY\b/i
|
181
|
-
when :group_by
|
182
|
-
normalized =~ /\bGROUP\s+BY\b/i
|
183
|
-
when :having
|
184
|
-
normalized =~ /\bHAVING\b/i
|
185
|
-
when :union
|
186
|
-
normalized =~ /\bUNION\b/i
|
187
|
-
when :window_function
|
188
|
-
normalized =~ /\bOVER\s*\(/i
|
189
|
-
else
|
190
|
-
false
|
191
|
-
end
|
192
|
-
end
|
193
|
-
end
|
194
|
-
end
|