134 lines
4 KiB
Ruby
134 lines
4 KiB
Ruby
# 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.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.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
|