dbviewer 0.8.1 → 0.9.1
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 +29 -16
- data/app/controllers/concerns/dbviewer/access_control_validation.rb +25 -0
- data/app/controllers/concerns/dbviewer/database_operations/table_operations.rb +3 -1
- data/app/controllers/concerns/dbviewer/database_operations.rb +12 -0
- data/app/controllers/dbviewer/api/entity_relationship_diagrams_controller.rb +26 -9
- data/app/controllers/dbviewer/api/tables_controller.rb +22 -6
- data/app/controllers/dbviewer/tables_controller.rb +23 -3
- data/lib/dbviewer/configuration.rb +29 -0
- data/lib/dbviewer/security/access_control.rb +80 -0
- data/lib/dbviewer/security/sql_parser.rb +465 -0
- data/lib/dbviewer/version.rb +1 -1
- data/lib/dbviewer.rb +5 -71
- data/lib/generators/dbviewer/templates/initializer.rb +25 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 897715943af7ce5d339aebb00b03bc13e1abc728c5e09d9615ec7189555cf014
|
4
|
+
data.tar.gz: 3ee632c6e1a6cc9fe5e5df08277520a0ea6a705e20a84bcab1756acd27a0f031
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 010a246eb09d2a6841c01bb3b87553b89ed4050a8781572a5b016894b208688d495744a1539636e8655c6ea7778449a2d0b9c0e204b0be5d7b68c2ee6ac883e3
|
7
|
+
data.tar.gz: e4142876a0867ebc55655469e8b85f240cc5a48b9b45ffb1d5bec89ab38939b95e996dcba1fa9b2555658cd1276415d24d14ae57ad98554b4f1a852beb8e0632
|
data/README.md
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-

|
2
|
-
|
3
1
|
# 👁️ DBViewer
|
4
2
|
|
5
3
|
> **The fastest way to visualize and explore your database**
|
@@ -7,7 +5,7 @@
|
|
7
5
|
DBViewer is a powerful Rails engine that provides a comprehensive interface to view and explore database tables, records, and schema.
|
8
6
|
It's designed for development, debugging, and database analysis, offering a clean and intuitive way to interact with your application's database.
|
9
7
|
|
10
|
-
<img width="1470" alt="image" src="https://github.com/user-attachments/assets/
|
8
|
+
<img width="1470" alt="image" src="https://github.com/user-attachments/assets/6bc68cfa-903e-49fb-a450-425317d6cbc0" />
|
11
9
|
|
12
10
|
## ✨ Features
|
13
11
|
|
@@ -25,16 +23,6 @@ It's designed for development, debugging, and database analysis, offering a clea
|
|
25
23
|
|
26
24
|
You can explore a live demo of DBViewer at [https://dbviewer-demo.wailantirajoh.tech/](https://dbviewer-demo.wailantirajoh.tech/). This demo showcases all the features of DBViewer on a sample database, allowing you to try out the tool before installing it in your own application.
|
27
25
|
|
28
|
-
## 📸 Screenshots
|
29
|
-
|
30
|
-
<details>
|
31
|
-
<summary>Click to see more screenshots</summary>
|
32
|
-
|
33
|
-
<img width="1470" alt="image" src="https://github.com/user-attachments/assets/7d708c14-5f78-42c4-b769-2167546b3aad" />
|
34
|
-
<img width="1470" alt="image" src="https://github.com/user-attachments/assets/f6d9a39a-a571-4328-908a-d96b3148f707" />
|
35
|
-
|
36
|
-
</details>
|
37
|
-
|
38
26
|
## 📥 Installation
|
39
27
|
|
40
28
|
Add this line to your application's Gemfile:
|
@@ -120,6 +108,15 @@ Dbviewer.configure do |config|
|
|
120
108
|
# Authentication options
|
121
109
|
# config.admin_credentials = { username: "admin", password: "your_secure_password" } # Basic HTTP auth credentials
|
122
110
|
|
111
|
+
# Table and Column Access Control
|
112
|
+
# config.access_control_mode = :whitelist # :whitelist, :blacklist, or :none (default)
|
113
|
+
# config.allowed_tables = ['users', 'orders', 'products'] # Only these tables accessible (whitelist mode)
|
114
|
+
# config.blocked_tables = ['admin_users', 'sensitive_data'] # These tables blocked (blacklist mode)
|
115
|
+
# config.blocked_columns = { # Hide sensitive columns from specific tables
|
116
|
+
# 'users' => ['password_digest', 'api_key', 'secret_token'],
|
117
|
+
# 'orders' => ['internal_notes']
|
118
|
+
# }
|
119
|
+
|
123
120
|
# Disable DBViewer completely
|
124
121
|
# config.disabled = Rails.env.production? # Disable in production
|
125
122
|
end
|
@@ -260,11 +257,11 @@ end
|
|
260
257
|
|
261
258
|
When disabled, all DBViewer routes return 404 responses, making it appear as if the tool was never installed. This is the recommended approach for production systems where database admin tools should not be accessible.
|
262
259
|
|
263
|
-
|
260
|
+
### 🔐 PII Data Masking
|
264
261
|
|
265
262
|
DBViewer includes built-in support for masking Personally Identifiable Information (PII) to protect sensitive data while allowing developers to browse database contents.
|
266
263
|
|
267
|
-
|
264
|
+
#### Quick Setup
|
268
265
|
|
269
266
|
Configure PII masking in your Rails initializer (e.g., `config/initializers/dbviewer.rb`):
|
270
267
|
|
@@ -296,7 +293,7 @@ Dbviewer.configure_pii do |pii|
|
|
296
293
|
end
|
297
294
|
```
|
298
295
|
|
299
|
-
|
296
|
+
#### Built-in Masking Types
|
300
297
|
|
301
298
|
- **`:email`** - Masks email addresses while preserving domain
|
302
299
|
- **`:phone`** - Masks phone numbers keeping first and last digits
|
@@ -305,6 +302,22 @@ end
|
|
305
302
|
- **`:full_redact`** - Completely redacts the value
|
306
303
|
- **`:partial`** - Partial masking (default behavior)
|
307
304
|
|
305
|
+
### Table and Column Access Control
|
306
|
+
|
307
|
+
DBViewer includes granular access control features to restrict access to specific tables and columns, providing an additional layer of security beyond basic authentication.
|
308
|
+
|
309
|
+
#### Access Control Modes
|
310
|
+
|
311
|
+
DBViewer supports three access control modes:
|
312
|
+
|
313
|
+
- **`:none`** (default) - All tables are accessible (current behavior)
|
314
|
+
- **`:whitelist`** - Only explicitly allowed tables are accessible (most secure)
|
315
|
+
- **`:blacklist`** - All tables except explicitly blocked ones are accessible
|
316
|
+
|
317
|
+
#### Whitelist Mode (Recommended for Production)
|
318
|
+
|
319
|
+
Whitelist mode is the most secure approach, where only explicitly allowed tables can be accessed:
|
320
|
+
|
308
321
|
### Generate Example Configuration
|
309
322
|
|
310
323
|
Use the generator to create an example PII configuration:
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
module AccessControlValidation
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
def validate_table_access(table_name)
|
8
|
+
return unless table_name.present?
|
9
|
+
|
10
|
+
unless access_control.table_accessible?(table_name)
|
11
|
+
Rails.logger.warn "DBViewer: Access denied for table '#{table_name}'"
|
12
|
+
flash[:alert] = access_control.access_violation_message(table_name)
|
13
|
+
redirect_to tables_path and return
|
14
|
+
else
|
15
|
+
Rails.logger.info "DBViewer: Access granted for table '#{table_name}'"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def validate_query_access(sql)
|
20
|
+
unless access_control.validate_query_table_access(sql)
|
21
|
+
raise SecurityError, "Query contains references to inaccessible tables"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -6,7 +6,9 @@ module Dbviewer
|
|
6
6
|
# Fetch all tables with their stats
|
7
7
|
# By default, don't include record counts for better performance on sidebar
|
8
8
|
def fetch_tables(include_record_counts = false)
|
9
|
-
database_manager.tables
|
9
|
+
all_table_names = database_manager.tables
|
10
|
+
filtered_table_names = filter_accessible_tables(all_table_names)
|
11
|
+
filtered_table_names.map do |table_name|
|
10
12
|
table_stats = { name: table_name }
|
11
13
|
table_stats[:record_count] = fetch_table_record_count(table_name) if include_record_counts
|
12
14
|
table_stats
|
@@ -17,5 +17,17 @@ module Dbviewer
|
|
17
17
|
def table_query_operations
|
18
18
|
@table_query_operations ||= database_manager.table_query_operations
|
19
19
|
end
|
20
|
+
|
21
|
+
def access_control
|
22
|
+
@access_control ||= Dbviewer::Security::AccessControl.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def filter_accessible_tables(tables)
|
26
|
+
access_control.filter_accessible_tables(tables)
|
27
|
+
end
|
28
|
+
|
29
|
+
def filter_accessible_columns(table_name, columns)
|
30
|
+
access_control.filter_accessible_columns(table_name, columns)
|
31
|
+
end
|
20
32
|
end
|
21
33
|
end
|
@@ -37,20 +37,37 @@ module Dbviewer
|
|
37
37
|
def extract_table_relationships(table_name)
|
38
38
|
metadata = fetch_table_metadata(table_name)
|
39
39
|
return [] unless metadata&.dig(:foreign_keys)&.present?
|
40
|
-
|
41
|
-
metadata[:foreign_keys].
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
40
|
+
table_names = @tables.map { |t| t[:name] }
|
41
|
+
metadata[:foreign_keys].filter_map do |fk|
|
42
|
+
# Only include relationship if target table is also accessible
|
43
|
+
if table_names.include?(fk[:to_table])
|
44
|
+
{
|
45
|
+
from_table: table_name,
|
46
|
+
to_table: fk[:to_table],
|
47
|
+
from_column: fk[:column],
|
48
|
+
to_column: fk[:primary_key],
|
49
|
+
name: fk[:name]
|
50
|
+
}
|
51
|
+
end
|
49
52
|
end
|
50
53
|
rescue => e
|
51
54
|
Rails.logger.error("[DBViewer] Error fetching relationships for #{table_name}: #{e.message}")
|
52
55
|
[] # Return empty array to continue processing other tables
|
53
56
|
end
|
57
|
+
|
58
|
+
# Override to filter relationships to only accessible tables
|
59
|
+
def fetch_table_relationships(tables)
|
60
|
+
table_names = tables.map { |t| t[:name] }
|
61
|
+
|
62
|
+
# Get all relationships from accessible tables
|
63
|
+
all_relationships = super(tables)
|
64
|
+
|
65
|
+
# Filter to only include relationships where both source and target tables are accessible
|
66
|
+
all_relationships.select do |relationship|
|
67
|
+
table_names.include?(relationship[:from_table]) &&
|
68
|
+
table_names.include?(relationship[:to_table])
|
69
|
+
end
|
70
|
+
end
|
54
71
|
end
|
55
72
|
end
|
56
73
|
end
|
@@ -2,18 +2,25 @@ module Dbviewer
|
|
2
2
|
module Api
|
3
3
|
class TablesController < BaseController
|
4
4
|
def index
|
5
|
-
|
6
|
-
render_success(total_tables: tables_count)
|
5
|
+
render_success(total_tables: fetch_tables.length)
|
7
6
|
end
|
8
7
|
|
9
8
|
def show
|
10
|
-
|
9
|
+
table_name = params[:id]
|
10
|
+
|
11
|
+
# Check if table is accessible
|
12
|
+
unless access_control.table_accessible?(table_name)
|
13
|
+
render_error(access_control.access_violation_message(table_name), :forbidden)
|
14
|
+
return
|
15
|
+
end
|
16
|
+
|
17
|
+
table_stats = fetch_table_stats(table_name)
|
11
18
|
render_success(**table_stats)
|
12
19
|
end
|
13
20
|
|
14
21
|
def records
|
15
|
-
|
16
|
-
render_success(
|
22
|
+
all_tables_stats = fetch_tables_stats
|
23
|
+
render_success(all_tables_stats)
|
17
24
|
end
|
18
25
|
|
19
26
|
def relationships_count
|
@@ -25,14 +32,23 @@ module Dbviewer
|
|
25
32
|
table_name = params[:id]
|
26
33
|
record_id = params[:record_id]
|
27
34
|
|
35
|
+
# Check if table is accessible
|
36
|
+
unless access_control.table_accessible?(table_name)
|
37
|
+
render_error("Access denied: Table '#{table_name}' is not accessible", :forbidden)
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
28
41
|
reverse_foreign_keys = fetch_table_metadata(table_name).dig(:reverse_foreign_keys) || []
|
29
42
|
relationship_counts = reverse_foreign_keys.map do |rel|
|
43
|
+
# Only include relationships to accessible tables
|
44
|
+
next unless access_control.table_accessible?(rel[:from_table])
|
45
|
+
|
30
46
|
{
|
31
47
|
table: rel[:from_table],
|
32
48
|
foreign_key: rel[:column],
|
33
49
|
count: database_manager.get_model_for(rel[:from_table]).where(rel[:column] => record_id).count
|
34
50
|
}
|
35
|
-
end
|
51
|
+
end.compact
|
36
52
|
|
37
53
|
render_success({
|
38
54
|
table_name: table_name,
|
@@ -1,11 +1,16 @@
|
|
1
1
|
module Dbviewer
|
2
2
|
class TablesController < ApplicationController
|
3
|
+
include Dbviewer::AccessControlValidation
|
4
|
+
|
3
5
|
before_action :set_table_name, except: [ :index ]
|
6
|
+
before_action :validate_table, only: [ :show, :query, :export_csv ]
|
4
7
|
before_action :set_query_filters, only: [ :show, :export_csv ]
|
5
8
|
before_action :set_global_filters, only: [ :show, :export_csv ]
|
6
9
|
|
7
10
|
def index
|
8
|
-
@tables =
|
11
|
+
@tables = @tables.map do |table|
|
12
|
+
table.merge(record_count: fetch_table_record_count(table[:name]))
|
13
|
+
end
|
9
14
|
end
|
10
15
|
|
11
16
|
def show
|
@@ -21,13 +26,24 @@ module Dbviewer
|
|
21
26
|
@total_count = datatable_data[:total_count]
|
22
27
|
@records = datatable_data[:records]
|
23
28
|
@total_pages = datatable_data[:total_pages]
|
24
|
-
@columns = datatable_data[:columns]
|
29
|
+
@columns = filter_accessible_columns(@table_name, datatable_data[:columns])
|
25
30
|
@metadata = datatable_data[:metadata]
|
26
31
|
end
|
27
32
|
|
28
33
|
def query
|
29
|
-
|
34
|
+
all_columns = fetch_table_columns(@table_name)
|
35
|
+
@columns = filter_accessible_columns(@table_name, all_columns)
|
30
36
|
@query = prepare_query(@table_name, params[:query])
|
37
|
+
|
38
|
+
if @query.present?
|
39
|
+
begin
|
40
|
+
validate_query_access(@query)
|
41
|
+
rescue SecurityError => e
|
42
|
+
flash[:alert] = e.message
|
43
|
+
render :query and return
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
31
47
|
@records = execute_query(@query)
|
32
48
|
|
33
49
|
render :query
|
@@ -64,6 +80,10 @@ module Dbviewer
|
|
64
80
|
@table_name = params[:id]
|
65
81
|
end
|
66
82
|
|
83
|
+
def validate_table
|
84
|
+
validate_table_access(@table_name)
|
85
|
+
end
|
86
|
+
|
67
87
|
def set_query_filters
|
68
88
|
@current_page = [ 1, params[:page].to_i ].max
|
69
89
|
@per_page = params[:per_page] ? params[:per_page].to_i : Dbviewer.configuration.default_per_page
|
@@ -77,6 +77,29 @@ module Dbviewer
|
|
77
77
|
# }
|
78
78
|
attr_accessor :custom_pii_masks
|
79
79
|
|
80
|
+
# Table access control - whitelist approach (more secure)
|
81
|
+
# Only tables listed here will be accessible
|
82
|
+
# @example ['users', 'orders', 'products']
|
83
|
+
attr_accessor :allowed_tables
|
84
|
+
|
85
|
+
# Table access control - blacklist approach
|
86
|
+
# Tables listed here will be blocked from access
|
87
|
+
# @example ['admin_users', 'sensitive_data', 'audit_logs']
|
88
|
+
attr_accessor :blocked_tables
|
89
|
+
|
90
|
+
# Column access control - hide sensitive columns
|
91
|
+
# @example {
|
92
|
+
# 'users' => ['password_digest', 'api_key', 'secret_token'],
|
93
|
+
# 'orders' => ['internal_notes']
|
94
|
+
# }
|
95
|
+
attr_accessor :blocked_columns
|
96
|
+
|
97
|
+
# Access control mode: :whitelist, :blacklist, or :none
|
98
|
+
# :whitelist - only allowed_tables are accessible (most secure)
|
99
|
+
# :blacklist - all tables except blocked_tables are accessible
|
100
|
+
# :none - all tables accessible (current behavior)
|
101
|
+
attr_accessor :access_control_mode
|
102
|
+
|
80
103
|
def initialize
|
81
104
|
@per_page_options = [ 10, 20, 50, 100 ]
|
82
105
|
@default_per_page = 20
|
@@ -103,6 +126,12 @@ module Dbviewer
|
|
103
126
|
@pii_rules = {}
|
104
127
|
@enable_pii_masking = true
|
105
128
|
@custom_pii_masks = {}
|
129
|
+
|
130
|
+
# Initialize access control settings
|
131
|
+
@allowed_tables = []
|
132
|
+
@blocked_tables = []
|
133
|
+
@blocked_columns = {}
|
134
|
+
@access_control_mode = :none # Default to current behavior
|
106
135
|
end
|
107
136
|
end
|
108
137
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
module Security
|
3
|
+
# Access control service to validate table and column access
|
4
|
+
class AccessControl
|
5
|
+
def initialize(config = nil)
|
6
|
+
@config = config || Dbviewer.configuration
|
7
|
+
@sql_parser = SqlParser.new
|
8
|
+
end
|
9
|
+
|
10
|
+
# Check if a table is accessible based on current access control mode
|
11
|
+
# @param table_name [String] Name of the table to check
|
12
|
+
# @return [Boolean] true if table is accessible, false otherwise
|
13
|
+
def table_accessible?(table_name)
|
14
|
+
return true if @config.access_control_mode == :none
|
15
|
+
|
16
|
+
case @config.access_control_mode
|
17
|
+
when :whitelist
|
18
|
+
@config.allowed_tables.include?(table_name.to_s)
|
19
|
+
when :blacklist
|
20
|
+
!@config.blocked_tables.include?(table_name.to_s)
|
21
|
+
else
|
22
|
+
true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Get list of accessible tables based on access control settings
|
27
|
+
# @param all_tables [Array<String>] List of all available tables
|
28
|
+
# @return [Array<String>] Filtered list of accessible tables
|
29
|
+
def filter_accessible_tables(all_tables)
|
30
|
+
return all_tables if @config.access_control_mode == :none
|
31
|
+
|
32
|
+
all_tables.select { |table| table_accessible?(table) }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Get list of accessible columns for a table
|
36
|
+
# @param table_name [String] Name of the table
|
37
|
+
# @param all_columns [Array<String>] List of all columns in the table
|
38
|
+
# @return [Array<String>] Filtered list of accessible columns
|
39
|
+
def filter_accessible_columns(table_name, all_columns)
|
40
|
+
blocked_columns = @config.blocked_columns[table_name.to_s] || []
|
41
|
+
all_columns.reject { |column| blocked_columns.include?(column.to_s) }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Validate if a SQL query only accesses allowed tables
|
45
|
+
# @param sql [String] The SQL query to validate
|
46
|
+
# @return [Boolean] true if query only accesses allowed tables
|
47
|
+
def validate_query_table_access(sql)
|
48
|
+
return true if @config.access_control_mode == :none
|
49
|
+
|
50
|
+
# Extract table names from the SQL query using the SQL parser
|
51
|
+
extracted_tables = @sql_parser.extract_table_names(sql)
|
52
|
+
|
53
|
+
# Check if all extracted tables are accessible
|
54
|
+
extracted_tables.all? { |table| table_accessible?(table) }
|
55
|
+
end
|
56
|
+
|
57
|
+
# Get access control violation message
|
58
|
+
# @param table_name [String] Name of the table that was blocked
|
59
|
+
# @return [String] Error message explaining the access violation
|
60
|
+
def access_violation_message(table_name = nil)
|
61
|
+
case @config.access_control_mode
|
62
|
+
when :whitelist
|
63
|
+
if table_name
|
64
|
+
"Access denied: Table '#{table_name}' is not in the allowed tables list"
|
65
|
+
else
|
66
|
+
"Access denied: Only the following tables are accessible: #{@config.allowed_tables.join(', ')}"
|
67
|
+
end
|
68
|
+
when :blacklist
|
69
|
+
if table_name
|
70
|
+
"Access denied: Table '#{table_name}' is blocked from access"
|
71
|
+
else
|
72
|
+
"Access denied: The following tables are blocked: #{@config.blocked_tables.join(', ')}"
|
73
|
+
end
|
74
|
+
else
|
75
|
+
"Access denied: Table access is restricted"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,465 @@
|
|
1
|
+
module Dbviewer
|
2
|
+
module Security
|
3
|
+
# SQL parser for extracting table names from SQL queries
|
4
|
+
# Handles complex SQL including CTEs, subqueries, joins, and DML operations
|
5
|
+
class SqlParser
|
6
|
+
require "set"
|
7
|
+
|
8
|
+
# Parse SQL query and extract all table names
|
9
|
+
# @param sql [String] The SQL query to parse
|
10
|
+
# @return [Array<String>] List of table names found in the query
|
11
|
+
def self.extract_table_names(sql)
|
12
|
+
new.extract_table_names(sql)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Parse SQL query and extract all table names
|
16
|
+
# @param sql [String] The SQL query to parse
|
17
|
+
# @return [Array<String>] List of table names found in the query
|
18
|
+
def extract_table_names(sql)
|
19
|
+
return [] if sql.nil? || sql.strip.empty?
|
20
|
+
|
21
|
+
# Remove comments and normalize whitespace
|
22
|
+
cleaned_sql = clean_sql(sql)
|
23
|
+
|
24
|
+
# Use a more sophisticated approach to handle complex queries
|
25
|
+
table_names = Set.new
|
26
|
+
|
27
|
+
# Split by semicolons to handle multiple statements
|
28
|
+
statements = cleaned_sql.split(";").map(&:strip).reject(&:empty?)
|
29
|
+
|
30
|
+
statements.each do |statement|
|
31
|
+
table_names.merge(extract_tables_from_statement(statement))
|
32
|
+
end
|
33
|
+
|
34
|
+
table_names.to_a.compact
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# Clean SQL by removing comments and normalizing whitespace
|
40
|
+
# Properly handles string literals to avoid removing content inside strings
|
41
|
+
# @param sql [String] The SQL query to clean
|
42
|
+
# @return [String] Cleaned SQL
|
43
|
+
def clean_sql(sql)
|
44
|
+
result = []
|
45
|
+
i = 0
|
46
|
+
in_single_quote = false
|
47
|
+
in_double_quote = false
|
48
|
+
in_single_line_comment = false
|
49
|
+
in_multi_line_comment = false
|
50
|
+
|
51
|
+
while i < sql.length
|
52
|
+
char = sql[i]
|
53
|
+
next_char = i + 1 < sql.length ? sql[i + 1] : nil
|
54
|
+
|
55
|
+
# Handle string literals first (highest priority)
|
56
|
+
if !in_single_line_comment && !in_multi_line_comment
|
57
|
+
case char
|
58
|
+
when "'"
|
59
|
+
if !in_double_quote
|
60
|
+
# Handle escaped single quotes
|
61
|
+
if in_single_quote && next_char == "'"
|
62
|
+
result << char << next_char
|
63
|
+
i += 2
|
64
|
+
next
|
65
|
+
else
|
66
|
+
in_single_quote = !in_single_quote
|
67
|
+
result << char
|
68
|
+
end
|
69
|
+
else
|
70
|
+
result << char
|
71
|
+
end
|
72
|
+
when '"'
|
73
|
+
if !in_single_quote
|
74
|
+
# Handle escaped double quotes
|
75
|
+
if in_double_quote && next_char == '"'
|
76
|
+
result << char << next_char
|
77
|
+
i += 2
|
78
|
+
next
|
79
|
+
else
|
80
|
+
in_double_quote = !in_double_quote
|
81
|
+
result << char
|
82
|
+
end
|
83
|
+
else
|
84
|
+
result << char
|
85
|
+
end
|
86
|
+
when "-"
|
87
|
+
# Check for single-line comment start
|
88
|
+
if !in_single_quote && !in_double_quote && next_char == "-"
|
89
|
+
in_single_line_comment = true
|
90
|
+
i += 2
|
91
|
+
next
|
92
|
+
else
|
93
|
+
result << char
|
94
|
+
end
|
95
|
+
when "/"
|
96
|
+
# Check for multi-line comment start
|
97
|
+
if !in_single_quote && !in_double_quote && next_char == "*"
|
98
|
+
in_multi_line_comment = true
|
99
|
+
i += 2
|
100
|
+
next
|
101
|
+
else
|
102
|
+
result << char
|
103
|
+
end
|
104
|
+
when "*"
|
105
|
+
# Check for multi-line comment end
|
106
|
+
if in_multi_line_comment && next_char == "/"
|
107
|
+
in_multi_line_comment = false
|
108
|
+
i += 2
|
109
|
+
next
|
110
|
+
elsif !in_single_line_comment && !in_multi_line_comment
|
111
|
+
result << char
|
112
|
+
end
|
113
|
+
when "\n", "\r"
|
114
|
+
# End single-line comment
|
115
|
+
if in_single_line_comment
|
116
|
+
in_single_line_comment = false
|
117
|
+
result << char
|
118
|
+
elsif !in_multi_line_comment
|
119
|
+
result << char
|
120
|
+
end
|
121
|
+
else
|
122
|
+
# Regular character
|
123
|
+
if !in_single_line_comment && !in_multi_line_comment
|
124
|
+
result << char
|
125
|
+
end
|
126
|
+
end
|
127
|
+
else
|
128
|
+
# Inside comment - handle comment end conditions
|
129
|
+
case char
|
130
|
+
when "*"
|
131
|
+
if in_multi_line_comment && next_char == "/"
|
132
|
+
in_multi_line_comment = false
|
133
|
+
i += 2
|
134
|
+
next
|
135
|
+
end
|
136
|
+
when "\n", "\r"
|
137
|
+
if in_single_line_comment
|
138
|
+
in_single_line_comment = false
|
139
|
+
result << char
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
i += 1
|
145
|
+
end
|
146
|
+
|
147
|
+
# Normalize whitespace and strip
|
148
|
+
result.join.squeeze(" ").strip
|
149
|
+
end
|
150
|
+
|
151
|
+
# Extract table names from a single SQL statement
|
152
|
+
# @param sql [String] A single SQL statement
|
153
|
+
# @param cte_names [Set] Set of CTE names to exclude (for recursive calls)
|
154
|
+
# @return [Array<String>] List of table names
|
155
|
+
def extract_tables_from_statement(sql, cte_names = Set.new)
|
156
|
+
table_names = Set.new
|
157
|
+
|
158
|
+
# Handle CTEs (WITH clauses) first and collect CTE names
|
159
|
+
cte_data = extract_cte_tables_and_names(sql)
|
160
|
+
table_names.merge(cte_data[:table_names])
|
161
|
+
current_cte_names = cte_names + cte_data[:cte_names]
|
162
|
+
|
163
|
+
# Handle main query tables
|
164
|
+
table_names.merge(extract_main_query_tables(sql, current_cte_names))
|
165
|
+
|
166
|
+
# Handle subqueries recursively
|
167
|
+
table_names.merge(extract_subquery_tables(sql, current_cte_names))
|
168
|
+
|
169
|
+
# Remove CTE names from the final result since they're not actual database tables
|
170
|
+
(table_names - current_cte_names).to_a
|
171
|
+
end
|
172
|
+
|
173
|
+
# Extract table names from CTEs (WITH clauses) and return both actual tables and CTE names
|
174
|
+
# @param sql [String] SQL statement
|
175
|
+
# @return [Hash] Hash with :table_names and :cte_names arrays
|
176
|
+
def extract_cte_tables_and_names(sql)
|
177
|
+
table_names = Set.new
|
178
|
+
cte_names = Set.new
|
179
|
+
|
180
|
+
# Find all CTE names first using an improved approach
|
181
|
+
# Look for pattern: [schema.]cte_name AS (
|
182
|
+
# Handles quoted identifiers, schema-qualified names, and backticks
|
183
|
+
# Pattern breakdown:
|
184
|
+
# (?:(?:\w+|"[^"]+"|`[^`]+`)\.)? - Optional schema prefix (word, quoted, or backticked) followed by dot
|
185
|
+
# (\w+|"[^"]+"|`[^`]+`) - CTE name (word, double-quoted, or backticked)
|
186
|
+
# \s+AS\s*\( - " AS ("
|
187
|
+
cte_name_pattern = /(?:(?:\w+|"[^"]+"|`[^`]+`)\.)?(\w+|"[^"]+"|`[^`]+`)\s+AS\s*\(/i
|
188
|
+
|
189
|
+
sql.scan(cte_name_pattern) do |match|
|
190
|
+
cte_name = match[0]
|
191
|
+
# Clean up the CTE name by removing quotes if present
|
192
|
+
clean_cte_name = cte_name.gsub(/^["'`](.+)["'`]$/, '\1')
|
193
|
+
cte_names.add(clean_cte_name)
|
194
|
+
end
|
195
|
+
|
196
|
+
# Then extract table names from the CTE definitions
|
197
|
+
# Match WITH clauses (including RECURSIVE)
|
198
|
+
cte_pattern = /\bWITH\s+(?:RECURSIVE\s+)?(.+?)(?=\s+SELECT\s+[^(]|\s+INSERT\s+|\s+UPDATE\s+|\s+DELETE\s+)/mi
|
199
|
+
|
200
|
+
cte_match = sql.match(cte_pattern)
|
201
|
+
if cte_match
|
202
|
+
cte_definitions = cte_match[1]
|
203
|
+
|
204
|
+
# Find all subqueries within the CTE definitions and extract tables from them
|
205
|
+
depth = 0
|
206
|
+
start_pos = nil
|
207
|
+
i = 0
|
208
|
+
|
209
|
+
while i < cte_definitions.length
|
210
|
+
char = cte_definitions[i]
|
211
|
+
|
212
|
+
if char == "(" && cte_definitions[i..-1] =~ /^\(\s*SELECT/i
|
213
|
+
if depth == 0
|
214
|
+
start_pos = i + 1
|
215
|
+
end
|
216
|
+
depth += 1
|
217
|
+
elsif char == ")" && depth > 0
|
218
|
+
depth -= 1
|
219
|
+
if depth == 0 && start_pos
|
220
|
+
subquery = cte_definitions[start_pos...i]
|
221
|
+
# Recursively extract tables from the CTE subquery
|
222
|
+
table_names.merge(extract_tables_from_statement(subquery, cte_names))
|
223
|
+
start_pos = nil
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
i += 1
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
{
|
232
|
+
table_names: table_names.to_a,
|
233
|
+
cte_names: cte_names.to_a
|
234
|
+
}
|
235
|
+
end
|
236
|
+
|
237
|
+
# Extract table names from main query operations
|
238
|
+
# @param sql [String] SQL statement
|
239
|
+
# @param cte_names [Set] Set of CTE names to exclude
|
240
|
+
# @return [Array<String>] List of table names
|
241
|
+
def extract_main_query_tables(sql, cte_names = Set.new)
|
242
|
+
table_names = Set.new
|
243
|
+
|
244
|
+
# FROM clauses (including table-valued functions and subqueries)
|
245
|
+
table_names.merge(extract_from_tables(sql, cte_names))
|
246
|
+
|
247
|
+
# JOIN clauses
|
248
|
+
table_names.merge(extract_join_tables(sql, cte_names))
|
249
|
+
|
250
|
+
# INSERT INTO
|
251
|
+
table_names.merge(extract_insert_tables(sql, cte_names))
|
252
|
+
|
253
|
+
# UPDATE (including UPDATE ... FROM)
|
254
|
+
table_names.merge(extract_update_tables(sql, cte_names))
|
255
|
+
|
256
|
+
# DELETE FROM (including DELETE ... FROM ... JOIN)
|
257
|
+
table_names.merge(extract_delete_tables(sql, cte_names))
|
258
|
+
|
259
|
+
# UNION, INTERSECT, EXCEPT
|
260
|
+
table_names.merge(extract_set_operation_tables(sql, cte_names))
|
261
|
+
|
262
|
+
table_names.to_a
|
263
|
+
end
|
264
|
+
|
265
|
+
# Extract table names from FROM clauses
|
266
|
+
# @param sql [String] SQL statement
|
267
|
+
# @param cte_names [Set] Set of CTE names to exclude
|
268
|
+
# @return [Array<String>] List of table names
|
269
|
+
def extract_from_tables(sql, cte_names = Set.new)
|
270
|
+
table_names = Set.new
|
271
|
+
|
272
|
+
# Match FROM clause with various patterns
|
273
|
+
# Handle: FROM table, FROM schema.table, FROM "table", FROM (subquery), FROM function()
|
274
|
+
from_pattern = /\bFROM\s+(?![\(\s]*SELECT)((?:\w+\.)?(?:\\?"[^"\\]+\\?"|`[^`]+`|\w+)(?:\s*\([^)]*\))?)/i
|
275
|
+
|
276
|
+
sql.scan(from_pattern) do |match|
|
277
|
+
table_ref = match[0]
|
278
|
+
# Skip if it looks like a function call or subquery
|
279
|
+
unless table_ref.include?("(") && table_ref.include?(")")
|
280
|
+
table_name = extract_table_name_from_reference(table_ref)
|
281
|
+
# Only add if it's not a CTE name
|
282
|
+
table_names.add(table_name) if table_name && !cte_names.include?(table_name)
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
table_names.to_a
|
287
|
+
end
|
288
|
+
|
289
|
+
# Extract table names from JOIN clauses
|
290
|
+
# @param sql [String] SQL statement
|
291
|
+
# @param cte_names [Set] Set of CTE names to exclude
|
292
|
+
# @return [Array<String>] List of table names
|
293
|
+
def extract_join_tables(sql, cte_names = Set.new)
|
294
|
+
table_names = Set.new
|
295
|
+
|
296
|
+
# Match all types of JOINs
|
297
|
+
join_pattern = /\b(?:INNER\s+|LEFT\s+(?:OUTER\s+)?|RIGHT\s+(?:OUTER\s+)?|FULL\s+(?:OUTER\s+)?|CROSS\s+)?JOIN\s+(?![\(\s]*SELECT)((?:\w+\.)?(?:\\?"[^"\\]+\\?"|`[^`]+`|\w+))/i
|
298
|
+
|
299
|
+
sql.scan(join_pattern) do |match|
|
300
|
+
table_ref = match[0]
|
301
|
+
table_name = extract_table_name_from_reference(table_ref)
|
302
|
+
# Only add if it's not a CTE name
|
303
|
+
table_names.add(table_name) if table_name && !cte_names.include?(table_name)
|
304
|
+
end
|
305
|
+
|
306
|
+
table_names.to_a
|
307
|
+
end
|
308
|
+
|
309
|
+
# Extract table names from INSERT statements
|
310
|
+
# @param sql [String] SQL statement
|
311
|
+
# @param cte_names [Set] Set of CTE names to exclude
|
312
|
+
# @return [Array<String>] List of table names
|
313
|
+
def extract_insert_tables(sql, cte_names = Set.new)
|
314
|
+
table_names = Set.new
|
315
|
+
|
316
|
+
# INSERT INTO table
|
317
|
+
insert_pattern = /\bINSERT\s+INTO\s+((?:\w+\.)?(?:\\?"[^"\\]+\\?"|`[^`]+`|\w+))/i
|
318
|
+
|
319
|
+
sql.scan(insert_pattern) do |match|
|
320
|
+
table_ref = match[0]
|
321
|
+
table_name = extract_table_name_from_reference(table_ref)
|
322
|
+
# Only add if it's not a CTE name
|
323
|
+
table_names.add(table_name) if table_name && !cte_names.include?(table_name)
|
324
|
+
end
|
325
|
+
|
326
|
+
table_names.to_a
|
327
|
+
end
|
328
|
+
|
329
|
+
# Extract table names from UPDATE statements
|
330
|
+
# @param sql [String] SQL statement
|
331
|
+
# @param cte_names [Set] Set of CTE names to exclude
|
332
|
+
# @return [Array<String>] List of table names
|
333
|
+
def extract_update_tables(sql, cte_names = Set.new)
|
334
|
+
table_names = Set.new
|
335
|
+
|
336
|
+
# UPDATE table SET ... or UPDATE table ... FROM other_table
|
337
|
+
update_pattern = /\bUPDATE\s+((?:\w+\.)?(?:\\?"[^"\\]+\\?"|`[^`]+`|\w+))/i
|
338
|
+
|
339
|
+
sql.scan(update_pattern) do |match|
|
340
|
+
table_ref = match[0]
|
341
|
+
table_name = extract_table_name_from_reference(table_ref)
|
342
|
+
# Only add if it's not a CTE name
|
343
|
+
table_names.add(table_name) if table_name && !cte_names.include?(table_name)
|
344
|
+
end
|
345
|
+
|
346
|
+
# UPDATE ... FROM (PostgreSQL style)
|
347
|
+
update_from_pattern = /\bUPDATE\s+\w+.*?\bFROM\s+((?:\w+\.)?(?:\\?"[^"\\]+\\?"|`[^`]+`|\w+))/i
|
348
|
+
|
349
|
+
sql.scan(update_from_pattern) do |match|
|
350
|
+
table_ref = match[0]
|
351
|
+
table_name = extract_table_name_from_reference(table_ref)
|
352
|
+
# Only add if it's not a CTE name
|
353
|
+
table_names.add(table_name) if table_name && !cte_names.include?(table_name)
|
354
|
+
end
|
355
|
+
|
356
|
+
table_names.to_a
|
357
|
+
end
|
358
|
+
|
359
|
+
# Extract table names from DELETE statements
|
360
|
+
# @param sql [String] SQL statement
|
361
|
+
# @param cte_names [Set] Set of CTE names to exclude
|
362
|
+
# @return [Array<String>] List of table names
|
363
|
+
def extract_delete_tables(sql, cte_names = Set.new)
|
364
|
+
table_names = Set.new
|
365
|
+
|
366
|
+
# DELETE FROM table
|
367
|
+
delete_pattern = /\bDELETE\s+FROM\s+((?:\w+\.)?(?:\\?"[^"\\]+\\?"|`[^`]+`|\w+))/i
|
368
|
+
|
369
|
+
sql.scan(delete_pattern) do |match|
|
370
|
+
table_ref = match[0]
|
371
|
+
table_name = extract_table_name_from_reference(table_ref)
|
372
|
+
# Only add if it's not a CTE name
|
373
|
+
table_names.add(table_name) if table_name && !cte_names.include?(table_name)
|
374
|
+
end
|
375
|
+
|
376
|
+
table_names.to_a
|
377
|
+
end
|
378
|
+
|
379
|
+
# Extract table names from set operations (UNION, INTERSECT, EXCEPT)
|
380
|
+
# @param sql [String] SQL statement
|
381
|
+
# @param cte_names [Set] Set of CTE names to exclude
|
382
|
+
# @return [Array<String>] List of table names
|
383
|
+
def extract_set_operation_tables(sql, cte_names = Set.new)
|
384
|
+
table_names = Set.new
|
385
|
+
|
386
|
+
# Split by set operations and process each part
|
387
|
+
parts = sql.split(/\b(?:UNION(?:\s+ALL)?|INTERSECT|EXCEPT)\b/i)
|
388
|
+
|
389
|
+
parts.each do |part|
|
390
|
+
table_names.merge(extract_from_tables(part, cte_names))
|
391
|
+
table_names.merge(extract_join_tables(part, cte_names))
|
392
|
+
end
|
393
|
+
|
394
|
+
table_names.to_a
|
395
|
+
end
|
396
|
+
|
397
|
+
# Extract table names from subqueries
|
398
|
+
# @param sql [String] SQL statement
|
399
|
+
# @param cte_names [Set] Set of CTE names to exclude
|
400
|
+
# @return [Array<String>] List of table names
|
401
|
+
def extract_subquery_tables(sql, cte_names = Set.new)
|
402
|
+
table_names = Set.new
|
403
|
+
|
404
|
+
# Find subqueries in parentheses
|
405
|
+
# This is a simplified approach - nested parentheses are complex to handle with regex
|
406
|
+
|
407
|
+
# Track parentheses depth to handle nested subqueries
|
408
|
+
depth = 0
|
409
|
+
start_pos = nil
|
410
|
+
i = 0
|
411
|
+
|
412
|
+
while i < sql.length
|
413
|
+
char = sql[i]
|
414
|
+
|
415
|
+
if char == "(" && sql[i..-1] =~ /^\(\s*SELECT/i
|
416
|
+
if depth == 0
|
417
|
+
start_pos = i + 1
|
418
|
+
end
|
419
|
+
depth += 1
|
420
|
+
elsif char == ")" && depth > 0
|
421
|
+
depth -= 1
|
422
|
+
if depth == 0 && start_pos
|
423
|
+
subquery = sql[start_pos...i]
|
424
|
+
# Recursively process subquery
|
425
|
+
table_names.merge(extract_tables_from_statement(subquery, cte_names))
|
426
|
+
start_pos = nil
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
i += 1
|
431
|
+
end
|
432
|
+
|
433
|
+
table_names.to_a
|
434
|
+
end
|
435
|
+
|
436
|
+
# Extract clean table name from a table reference
|
437
|
+
# @param table_ref [String] Table reference (e.g., "schema.table", "table", "\"table\"")
|
438
|
+
# @return [String, nil] Clean table name or nil if invalid
|
439
|
+
def extract_table_name_from_reference(table_ref)
|
440
|
+
return nil if table_ref.nil? || table_ref.strip.empty?
|
441
|
+
|
442
|
+
# Remove schema prefix (schema.table -> table)
|
443
|
+
if table_ref.include?(".")
|
444
|
+
parts = table_ref.split(".")
|
445
|
+
table_part = parts.last
|
446
|
+
else
|
447
|
+
table_part = table_ref
|
448
|
+
end
|
449
|
+
|
450
|
+
# Remove quotes and clean up
|
451
|
+
table_name = table_part.gsub(/^\\?"([^"\\]+)\\?"$/, '\1') # Remove escaped quotes
|
452
|
+
.gsub(/^"([^"]+)"$/, '\1') # Remove double quotes
|
453
|
+
.gsub(/^`([^`]+)`$/, '\1') # Remove backticks
|
454
|
+
.strip
|
455
|
+
|
456
|
+
# Validate table name (basic identifier rules)
|
457
|
+
if table_name =~ /^[a-zA-Z_][a-zA-Z0-9_]*$/
|
458
|
+
table_name
|
459
|
+
else
|
460
|
+
nil
|
461
|
+
end
|
462
|
+
end
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end
|
data/lib/dbviewer/version.rb
CHANGED
data/lib/dbviewer.rb
CHANGED
@@ -2,6 +2,8 @@ require "dbviewer/version"
|
|
2
2
|
require "dbviewer/configuration"
|
3
3
|
require "dbviewer/engine"
|
4
4
|
require "dbviewer/validator/sql"
|
5
|
+
require "dbviewer/security/sql_parser"
|
6
|
+
require "dbviewer/security/access_control"
|
5
7
|
|
6
8
|
require "dbviewer/storage/base"
|
7
9
|
require "dbviewer/storage/in_memory_storage"
|
@@ -82,26 +84,6 @@ module Dbviewer
|
|
82
84
|
@configuration = Configuration.new
|
83
85
|
end
|
84
86
|
|
85
|
-
# This class method will be called by the engine when it's appropriate
|
86
|
-
def setup
|
87
|
-
configure_query_logger
|
88
|
-
# Database connections will be validated when first accessed
|
89
|
-
Rails.logger.info "DBViewer: Initialized successfully (database connections will be validated on first access)"
|
90
|
-
end
|
91
|
-
|
92
|
-
# Validate database connections on-demand (called when first accessing DBViewer)
|
93
|
-
def validate_connections!
|
94
|
-
connection_errors = configuration.database_connections.filter_map do |key, config|
|
95
|
-
validate_single_connection(key, config)
|
96
|
-
end
|
97
|
-
|
98
|
-
if connection_errors.length == configuration.database_connections.length
|
99
|
-
raise "DBViewer could not connect to any configured database. Please check your database configuration."
|
100
|
-
end
|
101
|
-
|
102
|
-
connection_errors
|
103
|
-
end
|
104
|
-
|
105
87
|
# Configure PII masking rules using a block
|
106
88
|
#
|
107
89
|
# @example
|
@@ -118,62 +100,14 @@ module Dbviewer
|
|
118
100
|
yield(PiiConfigurator.new(configuration)) if block_given?
|
119
101
|
end
|
120
102
|
|
121
|
-
|
122
|
-
|
123
|
-
# Configure the query logger with current configuration settings
|
124
|
-
def configure_query_logger
|
103
|
+
# This class method will be called by the engine when it's appropriate
|
104
|
+
def setup
|
125
105
|
Dbviewer::Query::Logger.configure(
|
126
106
|
enable_query_logging: configuration.enable_query_logging,
|
127
107
|
query_logging_mode: configuration.query_logging_mode
|
128
108
|
)
|
129
|
-
end
|
130
|
-
|
131
|
-
# Validate a single database connection
|
132
|
-
# @param key [Symbol] The connection key
|
133
|
-
# @param config [Hash] The connection configuration
|
134
|
-
# @return [Hash, nil] Error hash if validation failed, nil if successful
|
135
|
-
def validate_single_connection(key, config)
|
136
|
-
connection_class = resolve_connection_class(config)
|
137
|
-
return { key: key, error: "No valid connection configuration found for #{key}" } unless connection_class
|
138
|
-
|
139
|
-
test_connection(connection_class, config, key)
|
140
|
-
store_resolved_connection(config, connection_class)
|
141
|
-
nil
|
142
|
-
rescue => e
|
143
|
-
Rails.logger.error "DBViewer could not connect to #{config[:name] || key.to_s} database: #{e.message}"
|
144
|
-
{ key: key, error: e.message }
|
145
|
-
end
|
146
|
-
|
147
|
-
# Resolve the connection class from configuration
|
148
|
-
# @param config [Hash] The connection configuration
|
149
|
-
# @return [Class, nil] The resolved connection class or nil
|
150
|
-
def resolve_connection_class(config)
|
151
|
-
return config[:connection] if config[:connection]
|
152
|
-
return nil unless config[:connection_class].is_a?(String)
|
153
|
-
|
154
|
-
begin
|
155
|
-
config[:connection_class].constantize
|
156
|
-
rescue NameError => e
|
157
|
-
Rails.logger.warn "DBViewer could not load connection class #{config[:connection_class]}: #{e.message}"
|
158
|
-
nil
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
# Test the database connection
|
163
|
-
# @param connection_class [Class] The connection class
|
164
|
-
# @param config [Hash] The connection configuration
|
165
|
-
# @param key [Symbol] The connection key
|
166
|
-
def test_connection(connection_class, config, key)
|
167
|
-
connection = connection_class.connection
|
168
|
-
adapter_name = connection.adapter_name
|
169
|
-
Rails.logger.info "DBViewer successfully connected to #{config[:name] || key.to_s} database (#{adapter_name})"
|
170
|
-
end
|
171
109
|
|
172
|
-
|
173
|
-
# @param config [Hash] The connection configuration
|
174
|
-
# @param connection_class [Class] The resolved connection class
|
175
|
-
def store_resolved_connection(config, connection_class)
|
176
|
-
config[:connection] = connection_class
|
110
|
+
Rails.logger.info "DBViewer: Initialized successfully"
|
177
111
|
end
|
178
112
|
end
|
179
113
|
end
|
@@ -19,6 +19,31 @@ Dbviewer.configure do |config|
|
|
19
19
|
# password: "your_secure_password"
|
20
20
|
# }
|
21
21
|
|
22
|
+
# Table and Column Access Control (Security Feature)
|
23
|
+
# Access control mode: :whitelist (most secure), :blacklist, or :none (default)
|
24
|
+
# config.access_control_mode = :whitelist
|
25
|
+
|
26
|
+
# Whitelist mode: Only these tables will be accessible
|
27
|
+
# config.allowed_tables = [
|
28
|
+
# 'users',
|
29
|
+
# 'orders',
|
30
|
+
# 'products',
|
31
|
+
# 'categories'
|
32
|
+
# ]
|
33
|
+
|
34
|
+
# Blacklist mode: All tables except these will be accessible
|
35
|
+
# config.blocked_tables = [
|
36
|
+
# 'admin_users',
|
37
|
+
# 'sensitive_data',
|
38
|
+
# 'audit_logs'
|
39
|
+
# ]
|
40
|
+
|
41
|
+
# Hide sensitive columns from specific tables
|
42
|
+
# config.blocked_columns = {
|
43
|
+
# 'users' => ['password_digest', 'api_key', 'secret_token'],
|
44
|
+
# 'orders' => ['internal_notes', 'admin_comments']
|
45
|
+
# }
|
46
|
+
|
22
47
|
# Multiple database connections configuration
|
23
48
|
# config.database_connections = {
|
24
49
|
# primary: {
|
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.
|
4
|
+
version: 0.9.1
|
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-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -95,6 +95,7 @@ files:
|
|
95
95
|
- app/assets/stylesheets/dbviewer/logs.css
|
96
96
|
- app/assets/stylesheets/dbviewer/query.css
|
97
97
|
- app/assets/stylesheets/dbviewer/table.css
|
98
|
+
- app/controllers/concerns/dbviewer/access_control_validation.rb
|
98
99
|
- app/controllers/concerns/dbviewer/database_operations.rb
|
99
100
|
- app/controllers/concerns/dbviewer/database_operations/connection_management.rb
|
100
101
|
- app/controllers/concerns/dbviewer/database_operations/data_export.rb
|
@@ -156,6 +157,8 @@ files:
|
|
156
157
|
- lib/dbviewer/query/logger.rb
|
157
158
|
- lib/dbviewer/query/notification_subscriber.rb
|
158
159
|
- lib/dbviewer/query/parser.rb
|
160
|
+
- lib/dbviewer/security/access_control.rb
|
161
|
+
- lib/dbviewer/security/sql_parser.rb
|
159
162
|
- lib/dbviewer/storage/base.rb
|
160
163
|
- lib/dbviewer/storage/file_storage.rb
|
161
164
|
- lib/dbviewer/storage/in_memory_storage.rb
|