dbviewer 0.7.11 โ†’ 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b357ba9437acaa807f1b31562bb7a1b76bfaefc7c3e2e6ccb3be4cfdbd627a0a
4
- data.tar.gz: 2adbc0f217154bad12cb3c7bfd711a7daa7dcbfa5b01538563ea49010cd6d2f3
3
+ metadata.gz: 988493e4778410960da76a39e233df19e74a8155393a2b7cbe39a5f073c5d905
4
+ data.tar.gz: 5fbc3a34833d8d7758b786623bf3a5767699f80bba68ba953415e791d63fe86f
5
5
  SHA512:
6
- metadata.gz: c24e471a8d6d3248ef88040cff072a7868b8e936bdb535888882497b9ac64396017bb42f7c7f100b68240c8a927e45b573f679e47bd6573c832fcd2e6adc837a
7
- data.tar.gz: a7be084a91cb9d090544d7a770ba3f1cf18e512a56a216b431a58672c09c4fd9b682cb6df768708c5fcc1bf85494a9738643e42f488103abe1aefc945ef2eda6
6
+ metadata.gz: 06c52e89a4342edcc7edfb15f91ef5c53bc528ddff3b56bb3f7a0a9d9967b2c75ecf41e8850bdab13a535e93dbdb9036bf545aac4e4b55aeb0a05b9fec0b1b2e
7
+ data.tar.gz: a18bd6e86c8dcd70dc851e2800d13cf150fe5a08ec1c4c606bd6d714e4dd62a3548b1bfc7e9510c8f8836fb01a783a5a6be32b5d6344bd0cd040c3da430e92b6
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  ![dbviewer](https://github.com/user-attachments/assets/665c1a65-aab3-4a7e-aa54-b42e871cb3d0)
2
2
 
3
3
  # ๐Ÿ‘๏ธ DBViewer
4
+
4
5
  > **The fastest way to visualize and explore your database**
5
6
 
6
7
  DBViewer is a powerful Rails engine that provides a comprehensive interface to view and explore database tables, records, and schema.
@@ -17,6 +18,7 @@ It's designed for development, debugging, and database analysis, offering a clea
17
18
  - **Data Browsing**
18
19
  - **SQL Queries**
19
20
  - **Multiple Database Connections**
21
+ - **PII Data Masking** - Protect sensitive data with configurable masking rules
20
22
  - **Enhanced UI Features**
21
23
 
22
24
  ## ๐Ÿงช Demo Application
@@ -258,6 +260,63 @@ end
258
260
 
259
261
  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.
260
262
 
263
+ ## ๐Ÿ” PII Data Masking
264
+
265
+ DBViewer includes built-in support for masking Personally Identifiable Information (PII) to protect sensitive data while allowing developers to browse database contents.
266
+
267
+ ### Quick Setup
268
+
269
+ Configure PII masking in your Rails initializer (e.g., `config/initializers/dbviewer.rb`):
270
+
271
+ ```ruby
272
+ # Enable PII masking (enabled by default)
273
+ Dbviewer.configure do |config|
274
+ config.enable_pii_masking = true
275
+ end
276
+
277
+ # Define masking rules
278
+ Dbviewer.configure_pii do |pii|
279
+ # Built-in masking types
280
+ pii.mask 'users.email', with: :email # john@example.com โ†’ jo***@example.com
281
+ pii.mask 'users.phone', with: :phone # +1234567890 โ†’ +1***90
282
+ pii.mask 'users.ssn', with: :ssn # 123456789 โ†’ ***-**-6789
283
+ pii.mask 'payments.card_number', with: :credit_card # 1234567890123456 โ†’ ****-****-****-3456
284
+ pii.mask 'users.api_key', with: :full_redact # any_value โ†’ ***REDACTED***
285
+
286
+ # Custom masking with lambda
287
+ pii.mask 'users.salary', with: ->(value) { value ? '$***,***' : value }
288
+
289
+ # Define reusable custom masks
290
+ pii.custom_mask :ip_mask, ->(value) {
291
+ return value if value.nil?
292
+ parts = value.split('.')
293
+ "#{parts[0]}.#{parts[1]}.***.***.***"
294
+ }
295
+ pii.mask 'logs.ip_address', with: :ip_mask
296
+ end
297
+ ```
298
+
299
+ ### Built-in Masking Types
300
+
301
+ - **`:email`** - Masks email addresses while preserving domain
302
+ - **`:phone`** - Masks phone numbers keeping first and last digits
303
+ - **`:ssn`** - Masks Social Security Numbers showing only last 4 digits
304
+ - **`:credit_card`** - Masks credit card numbers showing only last 4 digits
305
+ - **`:full_redact`** - Completely redacts the value
306
+ - **`:partial`** - Partial masking (default behavior)
307
+
308
+ ### Generate Example Configuration
309
+
310
+ Use the generator to create an example PII configuration:
311
+
312
+ ```bash
313
+ rails generate dbviewer:install
314
+ ```
315
+
316
+ This creates `config/initializers/dbviewer_pii_example.rb` with comprehensive examples.
317
+
318
+ For detailed PII masking documentation, see [PII_MASKING.md](docs/PII_MASKING.md).
319
+
261
320
  ## ๐Ÿ“ Security Note
262
321
 
263
322
  โš ๏ธ **Warning**: This engine provides direct access to your database contents, which contains sensitive information. Always protect it with HTTP Basic Authentication by configuring strong credentials as shown above.
@@ -40,8 +40,8 @@ module Dbviewer
40
40
  end
41
41
 
42
42
  # Render a cell that may include a foreign key link
43
- def render_table_cell(cell, column_name, metadata)
44
- cell_value = format_cell_value(cell)
43
+ def render_table_cell(cell, column_name, metadata, table_name = nil)
44
+ cell_value = format_cell_value(cell, table_name, column_name)
45
45
  foreign_key = metadata && metadata[:foreign_keys] ?
46
46
  metadata[:foreign_keys].find { |fk| fk[:column] == column_name } :
47
47
  nil
@@ -61,15 +61,15 @@ module Dbviewer
61
61
  end
62
62
 
63
63
  # Render a table row with cells
64
- def render_table_row(row, records, metadata)
64
+ def render_table_row(row, records, metadata, table_name = nil)
65
65
  content_tag(:tr) do
66
66
  # Start with action column (sticky first column)
67
- cells = [ render_action_cell(row, records.columns, metadata) ]
67
+ cells = [ render_action_cell(row, records.columns, metadata, table_name) ]
68
68
 
69
69
  # Add all data cells
70
70
  cells += row.each_with_index.map do |cell, cell_index|
71
71
  column_name = records.columns[cell_index]
72
- render_table_cell(cell, column_name, metadata)
72
+ render_table_cell(cell, column_name, metadata, table_name)
73
73
  end
74
74
 
75
75
  cells.join.html_safe
@@ -77,7 +77,7 @@ module Dbviewer
77
77
  end
78
78
 
79
79
  # Render the entire table body with rows
80
- def render_table_body(records, metadata)
80
+ def render_table_body(records, metadata, table_name = nil)
81
81
  if records.nil? || records.rows.nil? || records.empty?
82
82
  content_tag(:tbody) do
83
83
  content_tag(:tr) do
@@ -89,19 +89,20 @@ module Dbviewer
89
89
  else
90
90
  content_tag(:tbody) do
91
91
  records.rows.map do |row|
92
- render_table_row(row, records, metadata)
92
+ render_table_row(row, records, metadata, table_name)
93
93
  end.join.html_safe
94
94
  end
95
95
  end
96
96
  end
97
97
 
98
98
  # Render action buttons for a record
99
- def render_action_cell(row_data, columns, metadata = nil)
99
+ def render_action_cell(row_data, columns, metadata = nil, table_name = nil)
100
100
  data_attributes = {}
101
101
 
102
102
  # Create a hash of column_name: value pairs for data attributes
103
+ # Apply the same formatting logic used in table cells
103
104
  columns.each_with_index do |column_name, index|
104
- data_attributes[column_name] = row_data[index].to_s
105
+ data_attributes[column_name] = format_cell_value(row_data[index], table_name, column_name)
105
106
  end
106
107
 
107
108
  content_tag(:td, class: "text-center action-column") do
@@ -1,6 +1,11 @@
1
1
  module Dbviewer
2
2
  module FormattingHelper
3
- def format_cell_value(value)
3
+ def format_cell_value(value, table_name = nil, column_name = nil)
4
+ # Apply PII masking if configured
5
+ if table_name && column_name
6
+ value = Dbviewer::DataPrivacy::PiiMasker.mask_value(value, table_name, column_name)
7
+ end
8
+
4
9
  return "NULL" if value.nil?
5
10
  return format_default_value(value) unless value.is_a?(String)
6
11
 
@@ -142,8 +142,9 @@
142
142
  <% if @records.rows.any? %>
143
143
  <% @records.rows.each do |row| %>
144
144
  <tr>
145
- <% row.each do |cell| %>
146
- <td><%= format_cell_value(cell) %></td>
145
+ <% row.each_with_index do |cell, index| %>
146
+ <% column_name = @records.columns[index] %>
147
+ <td><%= format_cell_value(cell, @table_name, column_name) %></td>
147
148
  <% end %>
148
149
  </tr>
149
150
  <% end %>
@@ -136,7 +136,7 @@
136
136
  <%= render_sortable_header_row(@records, @order_by, @order_direction, @table_name, @current_page, @per_page, @column_filters) %>
137
137
  <%= render_column_filters_row(form, @records, @columns, @column_filters) %>
138
138
  </thead>
139
- <%= render_table_body(@records, @metadata) %>
139
+ <%= render_table_body(@records, @metadata, @table_name) %>
140
140
  </table>
141
141
  <% end %> <!-- End of form_with -->
142
142
  </div>
@@ -59,6 +59,24 @@ module Dbviewer
59
59
  # When enabled, all DBViewer routes will return 404 responses
60
60
  attr_accessor :disabled
61
61
 
62
+ # PII (Personally Identifiable Information) masking configuration
63
+ # Hash of table.column => masking rule
64
+ # @example {
65
+ # 'users.email' => :email,
66
+ # 'users.phone' => :phone,
67
+ # 'customers.ssn' => :custom_mask
68
+ # }
69
+ attr_accessor :pii_rules
70
+
71
+ # Enable/disable PII masking globally
72
+ attr_accessor :enable_pii_masking
73
+
74
+ # Custom PII masking blocks
75
+ # @example {
76
+ # custom_mask: ->(value) { value ? '***REDACTED***' : value }
77
+ # }
78
+ attr_accessor :custom_pii_masks
79
+
62
80
  def initialize
63
81
  @per_page_options = [ 10, 20, 50, 100 ]
64
82
  @default_per_page = 20
@@ -82,6 +100,9 @@ module Dbviewer
82
100
  }
83
101
  }
84
102
  @current_connection = :default
103
+ @pii_rules = {}
104
+ @enable_pii_masking = true
105
+ @custom_pii_masks = {}
85
106
  end
86
107
  end
87
108
  end
@@ -0,0 +1,125 @@
1
+ module Dbviewer
2
+ module DataPrivacy
3
+ class PiiMasker
4
+ BUILT_IN_MASKS = {
5
+ email: ->(value) { mask_email(value) },
6
+ phone: ->(value) { mask_phone(value) },
7
+ credit_card: ->(value) { mask_credit_card(value) },
8
+ ssn: ->(value) { mask_ssn(value) },
9
+ full_redact: ->(value) { value ? "***REDACTED***" : value },
10
+ partial: ->(value) { mask_partial(value) }
11
+ }.freeze
12
+
13
+ def self.mask_value(value, table_name, column_name)
14
+ return value unless should_mask?(table_name, column_name)
15
+
16
+ rule = get_masking_rule(table_name, column_name)
17
+ apply_mask(value, rule)
18
+ end
19
+
20
+ private
21
+
22
+ def self.should_mask?(table_name, column_name)
23
+ return false unless Dbviewer.configuration.enable_pii_masking
24
+ return false if Dbviewer.configuration.pii_rules.empty?
25
+
26
+ key = "#{table_name}.#{column_name}"
27
+ Dbviewer.configuration.pii_rules.key?(key)
28
+ end
29
+
30
+ def self.get_masking_rule(table_name, column_name)
31
+ key = "#{table_name}.#{column_name}"
32
+ Dbviewer.configuration.pii_rules[key]
33
+ end
34
+
35
+ def self.apply_mask(value, rule)
36
+ return value if value.nil?
37
+
38
+ case rule
39
+ when Symbol
40
+ apply_built_in_mask(value, rule)
41
+ when Proc
42
+ rule.call(value)
43
+ else
44
+ apply_built_in_mask(value, :partial)
45
+ end
46
+ rescue => e
47
+ Rails.logger.warn("PII masking failed for rule #{rule}: #{e.message}")
48
+ "***ERROR***"
49
+ end
50
+
51
+ def self.apply_built_in_mask(value, mask_type)
52
+ mask_proc = BUILT_IN_MASKS[mask_type]
53
+ if mask_proc
54
+ mask_proc.call(value)
55
+ else
56
+ # Check if it's a custom mask
57
+ custom_mask = Dbviewer.configuration.custom_pii_masks[mask_type]
58
+ if custom_mask && custom_mask.respond_to?(:call)
59
+ custom_mask.call(value)
60
+ else
61
+ mask_partial(value)
62
+ end
63
+ end
64
+ end
65
+
66
+ def self.mask_email(value)
67
+ return value unless value.to_s.include?("@")
68
+
69
+ parts = value.to_s.split("@")
70
+ username = parts[0]
71
+ domain = parts[1]
72
+
73
+ if username.length <= 1
74
+ masked_username = "*"
75
+ elsif username.length <= 2
76
+ masked_username = username
77
+ else
78
+ masked_username = "#{username[0..1]}***"
79
+ end
80
+
81
+ "#{masked_username}@#{domain}"
82
+ end
83
+
84
+ def self.mask_phone(value)
85
+ # Remove all non-digit characters
86
+ digits = value.to_s.gsub(/\D/, "")
87
+ return value if digits.length < 4
88
+
89
+ # Keep first and last 2 digits, mask the middle
90
+ if digits.length <= 6
91
+ "#{digits[0]}#{'*' * (digits.length - 2)}#{digits[-1]}"
92
+ else
93
+ "#{digits[0..1]}#{'*' * 3}#{digits[-2..-1]}"
94
+ end
95
+ end
96
+
97
+ def self.mask_credit_card(value)
98
+ digits = value.to_s.gsub(/\D/, "")
99
+ return value if digits.length < 4
100
+
101
+ # Show last 4 digits only
102
+ "****-****-****-#{digits[-4..-1]}"
103
+ end
104
+
105
+ def self.mask_ssn(value)
106
+ digits = value.to_s.gsub(/\D/, "")
107
+ return value if digits.length != 9
108
+
109
+ # Show last 4 digits only
110
+ "***-**-#{digits[-4..-1]}"
111
+ end
112
+
113
+ def self.mask_partial(value)
114
+ str = value.to_s
115
+ return str if str.length <= 2
116
+
117
+ if str.length <= 4
118
+ "#{str[0]}#{'*' * (str.length - 2)}#{str[-1]}"
119
+ else
120
+ "#{str[0..1]}#{'*' * 3}#{str[-2..-1]}"
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.7.11"
2
+ VERSION = "0.8.0"
3
3
  end
data/lib/dbviewer.rb CHANGED
@@ -23,11 +23,40 @@ require "dbviewer/database/metadata_manager"
23
23
  require "dbviewer/datatable/query_operations"
24
24
  require "dbviewer/datatable/query_params"
25
25
 
26
+ require "dbviewer/data_privacy/pii_masker"
27
+
26
28
  require "propshaft"
27
29
 
28
30
  module Dbviewer
29
31
  # Main module for the database viewer
30
32
 
33
+ # Helper class for configuring PII masking rules
34
+ class PiiConfigurator
35
+ def initialize(configuration)
36
+ @configuration = configuration
37
+ end
38
+
39
+ # Define a PII masking rule
40
+ # @param column_spec [String] Table and column in format "table.column"
41
+ # @param with [Symbol, Proc] Masking rule - either built-in symbol or custom proc
42
+ def mask(column_spec, with:)
43
+ @configuration.pii_rules[column_spec] = with
44
+ end
45
+
46
+ # Define a custom masking function
47
+ # @param name [Symbol] Name of the custom mask
48
+ # @param block [Proc] The masking function
49
+ def custom_mask(name, block)
50
+ @configuration.custom_pii_masks[name] = block
51
+ end
52
+
53
+ # Enable or disable PII masking globally
54
+ # @param enabled [Boolean] Whether to enable PII masking
55
+ def enabled=(enabled)
56
+ @configuration.enable_pii_masking = enabled
57
+ end
58
+ end
59
+
31
60
  class << self
32
61
  # Module accessor for configuration
33
62
  attr_writer :configuration
@@ -73,6 +102,22 @@ module Dbviewer
73
102
  connection_errors
74
103
  end
75
104
 
105
+ # Configure PII masking rules using a block
106
+ #
107
+ # @example
108
+ # Dbviewer.configure_pii do |pii|
109
+ # pii.mask 'users.email', with: :email
110
+ # pii.mask 'users.phone', with: :phone
111
+ # pii.mask 'users.ssn', with: :ssn
112
+ # pii.mask 'customers.credit_card', with: :credit_card
113
+ # pii.mask 'profiles.secret', with: :full_redact
114
+ # pii.mask 'accounts.api_key', with: ->(value) { value ? 'api_***' : value }
115
+ # pii.custom_mask :my_custom, ->(value) { "CUSTOM: #{value[0]}***" }
116
+ # end
117
+ def configure_pii
118
+ yield(PiiConfigurator.new(configuration)) if block_given?
119
+ end
120
+
76
121
  private
77
122
 
78
123
  # Configure the query logger with current configuration settings
@@ -7,6 +7,12 @@ module Dbviewer
7
7
  def copy_initializer_file
8
8
  copy_file "initializer.rb", "config/initializers/dbviewer.rb"
9
9
  end
10
+
11
+ def copy_pii_example
12
+ copy_file "pii_configuration_example.rb", "config/initializers/dbviewer_pii_example.rb"
13
+ say "Created example PII configuration at config/initializers/dbviewer_pii_example.rb", :green
14
+ say "Review and customize the PII masking rules, then rename to dbviewer_pii.rb to activate.", :yellow
15
+ end
10
16
  end
11
17
  end
12
18
  end
@@ -0,0 +1,99 @@
1
+ # Example DBViewer PII Configuration
2
+ #
3
+ # This file shows how to configure PII (Personally Identifiable Information) masking
4
+ # in DBViewer to protect sensitive data in your database views.
5
+ #
6
+ # Place this configuration in your Rails initializer file (e.g., config/initializers/dbviewer.rb)
7
+
8
+ Dbviewer.configure do |config|
9
+ # Enable/disable PII masking globally (default: true)
10
+ config.enable_pii_masking = true
11
+
12
+ # Other DBViewer configurations...
13
+ # config.default_per_page = 20
14
+ # config.enable_data_export = false
15
+ end
16
+
17
+ # Configure PII masking rules
18
+ Dbviewer.configure_pii do |pii|
19
+ # Built-in masking types:
20
+
21
+ # Email masking: john.doe@example.com -> jo***@example.com
22
+ pii.mask "users.email", with: :email
23
+ pii.mask "customers.email_address", with: :email
24
+
25
+ # Phone masking: +1234567890 -> +1***90
26
+ pii.mask "users.phone", with: :phone
27
+ pii.mask "profiles.mobile_number", with: :phone
28
+
29
+ # Social Security Number masking: 123456789 -> ***-**-6789
30
+ pii.mask "users.ssn", with: :ssn
31
+ pii.mask "employees.social_security", with: :ssn
32
+
33
+ # Credit card masking: 1234567890123456 -> ****-****-****-3456
34
+ pii.mask "payments.card_number", with: :credit_card
35
+
36
+ # Full redaction: any_value -> ***REDACTED***
37
+ pii.mask "users.api_key", with: :full_redact
38
+ pii.mask "accounts.secret_token", with: :full_redact
39
+
40
+ # Partial masking (default): john_doe -> jo***oe
41
+ pii.mask "users.username", with: :partial
42
+
43
+ # Custom masking with lambda/proc:
44
+ pii.mask "users.address", with: ->(value) {
45
+ return value if value.nil?
46
+ "#{value.split(' ').first} ***" # Show only first word
47
+ }
48
+
49
+ # Define custom masking functions that can be reused:
50
+ pii.custom_mask :ip_mask, ->(value) {
51
+ return value if value.nil?
52
+ parts = value.split(".")
53
+ return value if parts.length != 4
54
+ "#{parts[0]}.#{parts[1]}.***.***.***"
55
+ }
56
+
57
+ # Use custom mask:
58
+ pii.mask "logs.ip_address", with: :ip_mask
59
+ pii.mask "sessions.client_ip", with: :ip_mask
60
+
61
+ # More examples:
62
+
63
+ # Customer data
64
+ pii.mask "customers.first_name", with: :partial
65
+ pii.mask "customers.last_name", with: :partial
66
+ pii.mask "customers.date_of_birth", with: ->(value) {
67
+ return value if value.nil?
68
+ date = Date.parse(value.to_s) rescue nil
69
+ date ? "#{date.year}/***/**" : value
70
+ }
71
+
72
+ # Employee data
73
+ pii.mask "employees.salary", with: ->(value) { value ? "$***,***" : value }
74
+ pii.mask "employees.bank_account", with: :full_redact
75
+
76
+ # User profiles
77
+ pii.mask "profiles.biography", with: ->(value) {
78
+ return value if value.nil? || value.length <= 50
79
+ "#{value[0..50]}... [TRUNCATED FOR PRIVACY]"
80
+ }
81
+
82
+ # System logs with PII
83
+ pii.mask "audit_logs.user_data", with: :full_redact
84
+ pii.mask "error_logs.request_params", with: ->(value) {
85
+ # Redact JSON containing potential PII
86
+ return value if value.nil?
87
+ begin
88
+ JSON.parse(value)
89
+ "{ [REDACTED JSON DATA] }"
90
+ rescue
91
+ value
92
+ end
93
+ }
94
+ end
95
+
96
+ # You can also disable PII masking globally:
97
+ # Dbviewer.configure_pii do |pii|
98
+ # pii.enabled = false
99
+ # 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.7.11
4
+ version: 0.8.0
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-22 00:00:00.000000000 Z
11
+ date: 2025-06-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -144,6 +144,7 @@ files:
144
144
  - lib/dbviewer/cache/base.rb
145
145
  - lib/dbviewer/cache/in_memory.rb
146
146
  - lib/dbviewer/configuration.rb
147
+ - lib/dbviewer/data_privacy/pii_masker.rb
147
148
  - lib/dbviewer/database/dynamic_model_factory.rb
148
149
  - lib/dbviewer/database/manager.rb
149
150
  - lib/dbviewer/database/metadata_manager.rb
@@ -166,6 +167,7 @@ files:
166
167
  - lib/dbviewer/version.rb
167
168
  - lib/generators/dbviewer/install_generator.rb
168
169
  - lib/generators/dbviewer/templates/initializer.rb
170
+ - lib/generators/dbviewer/templates/pii_configuration_example.rb
169
171
  - lib/tasks/dbviewer_tasks.rake
170
172
  homepage: https://github.com/wailantirajoh/dbviewer
171
173
  licenses: