1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
|
# frozen_string_literal: true
module Gitlab
module Doctor
class Secrets
attr_reader :logger
def initialize(logger)
@logger = logger
end
def run!
logger.info "Checking encrypted values in the database"
Rails.application.eager_load! unless Rails.application.config.eager_load
models_with_attributes = Hash.new { |h, k| h[k] = [] }
models_with_encrypted_attributes.each do |model|
models_with_attributes[model] += model.attr_encrypted_attributes.keys
end
models_with_encrypted_tokens.each do |model|
models_with_attributes[model] += model.encrypted_token_authenticatable_fields
end
check_model_attributes(models_with_attributes)
logger.info "Done!"
end
private
# Skipping initializers may be needed if those attempt to access
# encrypted data on initialization and could fail because of it.
#
# format example:
# {
# model_class => {
# [
# { action: :create, filters: [:before, :filter_name1] },
# { action: :update, filters: [:after, :filter_name2] }
# ]
# }
# }
MODEL_INITIALIZERS_TO_SKIP = {
Integration => [
{ action: :initialize, filters: [:after, :initialize_properties] }
]
}.freeze
def check_model_attributes(models_with_attributes)
running_failures = 0
models_with_attributes.each do |model, attributes|
failures_per_row = Hash.new { |h, k| h[k] = [] }
with_skipped_callbacks_for(model) do
model.find_each do |data|
attributes.each do |att|
failures_per_row[data.id] << att unless valid_attribute?(data, att)
end
end
end
running_failures += failures_per_row.keys.count
output_failures_for_model(model, failures_per_row)
end
logger.info "Total: #{running_failures} row(s) affected".color(:blue)
end
def output_failures_for_model(model, failures)
status_color = failures.empty? ? :green : :red
logger.info "- #{model} failures: #{failures.count}".color(status_color)
failures.each do |row_id, attributes|
logger.debug " - #{model}[#{row_id}]: #{attributes.join(", ")}".color(:red)
end
end
def models_with_encrypted_attributes
all_models.select { |d| d.attr_encrypted_attributes.present? }
end
def models_with_encrypted_tokens
all_models.select do |d|
d.include?(TokenAuthenticatable) && d.encrypted_token_authenticatable_fields.present?
end
end
def all_models
@all_models ||= ApplicationRecord.descendants
end
def valid_attribute?(data, attr)
data.send(attr) # rubocop:disable GitlabSecurity/PublicSend
true
rescue OpenSSL::Cipher::CipherError, TypeError
false
rescue StandardError => e
logger.debug "> Something went wrong for #{data.class.name}[#{data.id}].#{attr}: #{e}".color(:red)
false
end
# WARNING: using this logic in other places than a Rake task will need a
# different approach, as simply setting the callback again is not thread-safe
def with_skipped_callbacks_for(model)
raise StandardError, 'can only be used in a Rake environment' unless Gitlab::Runtime.rake?
skip_callbacks_for_model(model)
yield
skip_callbacks_for_model(model, reset: true)
end
def skip_callbacks_for_model(model, reset: false)
MODEL_INITIALIZERS_TO_SKIP.each do |klass, initializers|
next unless model <= klass
initializers.each do |initializer|
if reset
model.set_callback(initializer[:action], *initializer[:filters])
else
model.skip_callback(initializer[:action], *initializer[:filters])
end
end
end
end
end
end
end
|