Merge pull request #10712 from colinux/rails-7-finalize-defaults
Rails 7.0 : finalise la migration des defaults
This commit is contained in:
commit
7c30ab87bc
8 changed files with 185 additions and 152 deletions
|
@ -7,6 +7,10 @@ class EncryptionService
|
||||||
password = Rails.application.secrets.secret_key_base
|
password = Rails.application.secrets.secret_key_base
|
||||||
key = ActiveSupport::KeyGenerator.new(password).generate_key(salt, len)
|
key = ActiveSupport::KeyGenerator.new(password).generate_key(salt, len)
|
||||||
@encryptor = ActiveSupport::MessageEncryptor.new(key)
|
@encryptor = ActiveSupport::MessageEncryptor.new(key)
|
||||||
|
|
||||||
|
# Remove after all encrypted attributes have been rotated.
|
||||||
|
legacy_key = ActiveSupport::KeyGenerator.new(password, hash_digest_class: OpenSSL::Digest::SHA1).generate_key(salt, len)
|
||||||
|
@encryptor.rotate legacy_key
|
||||||
end
|
end
|
||||||
|
|
||||||
def encrypt(value)
|
def encrypt(value)
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Maintenance
|
||||||
|
class RotateAPIParticulierTokenEncryptionTask < MaintenanceTasks::Task
|
||||||
|
def collection
|
||||||
|
# rubocop:disable DS/Unscoped
|
||||||
|
Procedure.unscoped.where.not(encrypted_api_particulier_token: nil)
|
||||||
|
# rubocop:enable DS/Unscoped
|
||||||
|
end
|
||||||
|
|
||||||
|
def process(procedure)
|
||||||
|
decrypted_token = procedure.api_particulier_token
|
||||||
|
|
||||||
|
procedure.api_particulier_token = decrypted_token
|
||||||
|
procedure.save!(validate: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def count
|
||||||
|
collection.count
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -13,7 +13,7 @@ Dotenv::Railtie.load
|
||||||
module TPS
|
module TPS
|
||||||
class Application < Rails::Application
|
class Application < Rails::Application
|
||||||
# Initialize configuration defaults for originally generated Rails version.
|
# Initialize configuration defaults for originally generated Rails version.
|
||||||
config.load_defaults 6.1
|
config.load_defaults 7.0
|
||||||
|
|
||||||
# Configuration for the application, engines, and railties goes here.
|
# Configuration for the application, engines, and railties goes here.
|
||||||
#
|
#
|
||||||
|
@ -62,6 +62,8 @@ module TPS
|
||||||
config.active_storage.queues.analysis = :active_storage_analysis
|
config.active_storage.queues.analysis = :active_storage_analysis
|
||||||
config.active_storage.queues.purge = :purge
|
config.active_storage.queues.purge = :purge
|
||||||
|
|
||||||
|
config.active_support.cache_format_version = 7.0
|
||||||
|
|
||||||
config.to_prepare do
|
config.to_prepare do
|
||||||
# Make main application helpers available in administrate
|
# Make main application helpers available in administrate
|
||||||
Administrate::ApplicationController.helper(TPS::Application.helpers)
|
Administrate::ApplicationController.helper(TPS::Application.helpers)
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
Rails.application.config.active_storage.service_urls_expire_in = 1.hour
|
Rails.application.config.active_storage.service_urls_expire_in = 1.hour
|
||||||
|
|
||||||
|
Rails.application.config.active_storage.variant_processor = :mini_magick
|
||||||
Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::ImageAnalyzer
|
Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::ImageAnalyzer
|
||||||
Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer
|
Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,24 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# TODO: Enable cookies rotation when new SHA256 will be enforced
|
# This cookie rotator converts cookies from the old SHA1 hash (Rails 6) to SHA256 hash (Rails 7 default).
|
||||||
# See new_framework_defaults_7.0.rb
|
# It should be kept enabled for approximately 1 month to ensure most users have their cookies rotated.
|
||||||
# key_generator_hash_digest_class = OpenSSL::Digest::SHA256 will be
|
# After this period, it can be safely removed.
|
||||||
#
|
# Without this rotator, all users would have been signed out.
|
||||||
# Rails.application.config.after_initialize do
|
Rails.application.config.after_initialize do
|
||||||
# Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies|
|
Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies|
|
||||||
# salt = Rails.application.config.action_dispatch.authenticated_encrypted_cookie_salt
|
authenticated_encrypted_cookie_salt = Rails.application.config.action_dispatch.authenticated_encrypted_cookie_salt
|
||||||
# secret_key_base = Rails.application.secret_key_base
|
signed_cookie_salt = Rails.application.config.action_dispatch.signed_cookie_salt
|
||||||
|
secret_key_base = Rails.application.secret_key_base
|
||||||
|
|
||||||
# key_generator = ActiveSupport::KeyGenerator.new(
|
key_generator = ActiveSupport::KeyGenerator.new(
|
||||||
# secret_key_base, iterations: 1000, hash_digest_class: OpenSSL::Digest::SHA1
|
secret_key_base, iterations: 1000, hash_digest_class: OpenSSL::Digest::SHA1 # Rails 6 hash
|
||||||
# )
|
)
|
||||||
# key_len = ActiveSupport::MessageEncryptor.key_len
|
key_len = ActiveSupport::MessageEncryptor.key_len
|
||||||
# secret = key_generator.generate_key(salt, key_len)
|
|
||||||
|
|
||||||
# cookies.rotate :encrypted, secret
|
old_encrypted_secret = key_generator.generate_key(authenticated_encrypted_cookie_salt, key_len)
|
||||||
# end
|
old_signed_secret = key_generator.generate_key(signed_cookie_salt)
|
||||||
# end
|
|
||||||
|
cookies.rotate :encrypted, old_encrypted_secret
|
||||||
|
cookies.rotate :signed, old_signed_secret
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -1,135 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# Be sure to restart your server when you modify this file.
|
|
||||||
#
|
|
||||||
# This file eases your Rails 7.0 framework defaults upgrade.
|
|
||||||
#
|
|
||||||
# Uncomment each configuration one by one to switch to the new default.
|
|
||||||
# Once your application is ready to run with all new defaults, you can remove
|
|
||||||
# this file and set the `config.load_defaults` to `7.0`.
|
|
||||||
#
|
|
||||||
# Read the Guide for Upgrading Ruby on Rails for more info on each option.
|
|
||||||
# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html
|
|
||||||
|
|
||||||
# `button_to` view helper will render `<button>` element, regardless of whether
|
|
||||||
# or not the content is passed as the first argument or as a block.
|
|
||||||
Rails.application.config.action_view.button_to_generates_button_tag = true
|
|
||||||
|
|
||||||
# `stylesheet_link_tag` view helper will not render the media attribute by default.
|
|
||||||
Rails.application.config.action_view.apply_stylesheet_media_default = false
|
|
||||||
|
|
||||||
# Change the digest class for the key generators to `OpenSSL::Digest::SHA256`.
|
|
||||||
# Changing this default means invalidate all encrypted messages generated by
|
|
||||||
# your application and, all the encrypted cookies. Only change this after you
|
|
||||||
# rotated all the messages using the key rotator.
|
|
||||||
#
|
|
||||||
# See upgrading guide for more information on how to build a rotator.
|
|
||||||
# https://guides.rubyonrails.org/v7.0/upgrading_ruby_on_rails.html
|
|
||||||
# Rails.application.config.active_support.key_generator_hash_digest_class = OpenSSL::Digest::SHA256
|
|
||||||
|
|
||||||
# Change the digest class for ActiveSupport::Digest.
|
|
||||||
# Changing this default means that for example Etags change and
|
|
||||||
# various cache keys leading to cache invalidation.
|
|
||||||
Rails.application.config.active_support.hash_digest_class = OpenSSL::Digest::SHA256
|
|
||||||
|
|
||||||
# Don't override ActiveSupport::TimeWithZone.name and use the default Ruby
|
|
||||||
# implementation.
|
|
||||||
Rails.application.config.active_support.remove_deprecated_time_with_zone_name = true
|
|
||||||
|
|
||||||
# Calls `Rails.application.executor.wrap` around test cases.
|
|
||||||
# This makes test cases behave closer to an actual request or job.
|
|
||||||
# Several features that are normally disabled in test, such as Active Record query cache
|
|
||||||
# and asynchronous queries will then be enabled.
|
|
||||||
Rails.application.config.active_support.executor_around_test_case = true
|
|
||||||
|
|
||||||
# Define the isolation level of most of Rails internal state.
|
|
||||||
# If you use a fiber based server or job processor, you should set it to `:fiber`.
|
|
||||||
# Otherwise the default of `:thread` if preferable.
|
|
||||||
Rails.application.config.active_support.isolation_level = :thread
|
|
||||||
|
|
||||||
# Set both the `:open_timeout` and `:read_timeout` values for `:smtp` delivery method.
|
|
||||||
Rails.application.config.action_mailer.smtp_timeout = 5
|
|
||||||
|
|
||||||
# The ActiveStorage video previewer will now use scene change detection to generate
|
|
||||||
# better preview images (rather than the previous default of using the first frame
|
|
||||||
# of the video).
|
|
||||||
Rails.application.config.active_storage.video_preview_arguments =
|
|
||||||
"-vf 'select=eq(n\\,0)+eq(key\\,1)+gt(scene\\,0.015),loop=loop=-1:size=2,trim=start_frame=1' -frames:v 1 -f image2"
|
|
||||||
|
|
||||||
# Automatically infer `inverse_of` for associations with a scope.
|
|
||||||
Rails.application.config.active_record.automatic_scope_inversing = true
|
|
||||||
|
|
||||||
# Raise when running tests if fixtures contained foreign key violations
|
|
||||||
Rails.application.config.active_record.verify_foreign_keys_for_fixtures = true
|
|
||||||
|
|
||||||
# Disable partial inserts.
|
|
||||||
# This default means that all columns will be referenced in INSERT queries
|
|
||||||
# regardless of whether they have a default or not.
|
|
||||||
Rails.application.config.active_record.partial_inserts = false
|
|
||||||
|
|
||||||
# Protect from open redirect attacks in `redirect_back_or_to` and `redirect_to`.
|
|
||||||
Rails.application.config.action_controller.raise_on_open_redirects = true
|
|
||||||
|
|
||||||
# Change the variant processor for Active Storage.
|
|
||||||
# Changing this default means updating all places in your code that
|
|
||||||
# generate variants to use image processing macros and ruby-vips
|
|
||||||
# operations. See the upgrading guide for detail on the changes required.
|
|
||||||
# The `:mini_magick` option is not deprecated; it's fine to keep using it.
|
|
||||||
Rails.application.config.active_storage.variant_processor = :mini_magick
|
|
||||||
|
|
||||||
# Enable parameter wrapping for JSON.
|
|
||||||
# Previously this was set in an initializer. It's fine to keep using that initializer if you've customized it.
|
|
||||||
# To disable parameter wrapping entirely, set this config to `false`.
|
|
||||||
Rails.application.config.action_controller.wrap_parameters_by_default = true
|
|
||||||
|
|
||||||
# Specifies whether generated namespaced UUIDs follow the RFC 4122 standard for namespace IDs provided as a
|
|
||||||
# `String` to `Digest::UUID.uuid_v3` or `Digest::UUID.uuid_v5` method calls.
|
|
||||||
#
|
|
||||||
# See https://guides.rubyonrails.org/configuring.html#config-active-support-use-rfc4122-namespaced-uuids for
|
|
||||||
# more information.
|
|
||||||
Rails.application.config.active_support.use_rfc4122_namespaced_uuids = true
|
|
||||||
|
|
||||||
# Change the default headers to disable browsers' flawed legacy XSS protection.
|
|
||||||
Rails.application.config.action_dispatch.default_headers = {
|
|
||||||
"X-Frame-Options" => "SAMEORIGIN",
|
|
||||||
"X-XSS-Protection" => "0",
|
|
||||||
"X-Content-Type-Options" => "nosniff",
|
|
||||||
"X-Download-Options" => "noopen",
|
|
||||||
"X-Permitted-Cross-Domain-Policies" => "none",
|
|
||||||
"Referrer-Policy" => "strict-origin-when-cross-origin"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ** Please read carefully, this must be configured in config/application.rb **
|
|
||||||
# Change the format of the cache entry.
|
|
||||||
# Changing this default means that all new cache entries added to the cache
|
|
||||||
# will have a different format that is not supported by Rails 6.1 applications.
|
|
||||||
# Only change this value after your application is fully deployed to Rails 7.0
|
|
||||||
# and you have no plans to rollback.
|
|
||||||
# When you're ready to change format, add this to `config/application.rb` (NOT this file):
|
|
||||||
# config.active_support.cache_format_version = 7.0
|
|
||||||
|
|
||||||
# Cookie serializer: 2 options
|
|
||||||
#
|
|
||||||
# If you're upgrading and haven't set `cookies_serializer` previously, your cookie serializer
|
|
||||||
# is `:marshal`. The default for new apps is `:json`.
|
|
||||||
#
|
|
||||||
# Rails.application.config.action_dispatch.cookies_serializer = :json
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# To migrate an existing application to the `:json` serializer, use the `:hybrid` option.
|
|
||||||
#
|
|
||||||
# Rails transparently deserializes existing (Marshal-serialized) cookies on read and
|
|
||||||
# re-writes them in the JSON format.
|
|
||||||
#
|
|
||||||
# It is fine to use `:hybrid` long term; you should do that until you're confident *all* your cookies
|
|
||||||
# have been converted to JSON. To keep using `:hybrid` long term, move this config to its own
|
|
||||||
# initializer or to `config/application.rb`.
|
|
||||||
#
|
|
||||||
# Rails.application.config.action_dispatch.cookies_serializer = :hybrid
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# If your cookies can't yet be serialized to JSON, keep using `:marshal` for backward-compatibility.
|
|
||||||
#
|
|
||||||
# If you have configured the serializer elsewhere, you can remove this section of the file.
|
|
||||||
#
|
|
||||||
# See https://guides.rubyonrails.org/action_controller_overview.html#cookies for more information.
|
|
|
@ -41,4 +41,89 @@ describe EncryptionService do
|
||||||
it { expect { subject }.to raise_exception StandardError }
|
it { expect { subject }.to raise_exception StandardError }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "key rotation" do
|
||||||
|
let(:password) { Rails.application.secrets.secret_key_base }
|
||||||
|
let(:salt) { Rails.application.secrets.encryption_service_salt }
|
||||||
|
let(:len) { ActiveSupport::MessageEncryptor.key_len }
|
||||||
|
let(:value) { "Sensitive information" }
|
||||||
|
|
||||||
|
let(:legacy_key) do
|
||||||
|
ActiveSupport::KeyGenerator.new(password, hash_digest_class: OpenSSL::Digest::SHA1)
|
||||||
|
.generate_key(salt, len)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:new_key) do
|
||||||
|
ActiveSupport::KeyGenerator.new(password)
|
||||||
|
.generate_key(salt, len)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:legacy_encryptor) { ActiveSupport::MessageEncryptor.new(legacy_key) }
|
||||||
|
let(:new_encryptor) { ActiveSupport::MessageEncryptor.new(new_key) }
|
||||||
|
|
||||||
|
describe "#decrypt" do
|
||||||
|
subject { EncryptionService.new.decrypt(encrypted_value) }
|
||||||
|
|
||||||
|
context "with a value encrypted using the legacy SHA1-based key" do
|
||||||
|
let(:encrypted_value) { legacy_encryptor.encrypt_and_sign(value) }
|
||||||
|
|
||||||
|
it "successfully decrypts the value" do
|
||||||
|
expect(subject).to eq(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with a value encrypted using the new SHA256-based key" do
|
||||||
|
let(:encrypted_value) { new_encryptor.encrypt_and_sign(value) }
|
||||||
|
|
||||||
|
it "successfully decrypts the value" do
|
||||||
|
expect(subject).to eq(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "transition from legacy to new encryption" do
|
||||||
|
let(:legacy_service) do
|
||||||
|
legacy_encryption_service = EncryptionService.new
|
||||||
|
legacy_encryption_service.instance_variable_set(:@encryptor, legacy_encryptor)
|
||||||
|
legacy_encryption_service
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:new_service) { EncryptionService.new }
|
||||||
|
|
||||||
|
it "can decrypt values encrypted with the legacy key" do
|
||||||
|
legacy_encrypted = legacy_service.encrypt(value)
|
||||||
|
expect(new_service.decrypt(legacy_encrypted)).to eq(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "uses the new key for new encryptions" do
|
||||||
|
new_encrypted = new_service.encrypt(value)
|
||||||
|
expect { legacy_encryptor.decrypt_and_verify(new_encrypted) }
|
||||||
|
.to raise_error(ActiveSupport::MessageEncryptor::InvalidMessage)
|
||||||
|
expect(new_encryptor.decrypt_and_verify(new_encrypted)).to eq(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "backwards compatibility" do
|
||||||
|
let(:value) { "Important data" }
|
||||||
|
let(:old_service) do # Test with a service encrypting data without rotation mechanism
|
||||||
|
Class.new do
|
||||||
|
def initialize(key)
|
||||||
|
@encryptor = ActiveSupport::MessageEncryptor.new(key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def encrypt(value)
|
||||||
|
@encryptor.encrypt_and_sign(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can decrypt values from a hypothetical old version without rotation" do
|
||||||
|
old_key = ActiveSupport::KeyGenerator.new(password, hash_digest_class: OpenSSL::Digest::SHA1)
|
||||||
|
.generate_key(salt, len)
|
||||||
|
old_encrypted = old_service.new(old_key).encrypt(value)
|
||||||
|
|
||||||
|
expect(EncryptionService.new.decrypt(old_encrypted)).to eq(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
module Maintenance
|
||||||
|
RSpec.describe RotateAPIParticulierTokenEncryptionTask do
|
||||||
|
describe "#process" do
|
||||||
|
subject { described_class.process(procedure) }
|
||||||
|
let(:token) { "secret-token-0123456789" }
|
||||||
|
let(:procedure) { create(:procedure) }
|
||||||
|
let(:legacy_encryption_service) do
|
||||||
|
EncryptionService.new.tap { |legacy_service|
|
||||||
|
legacy_key = ActiveSupport::KeyGenerator
|
||||||
|
.new(Rails.application.secrets.secret_key_base, hash_digest_class: OpenSSL::Digest::SHA1)
|
||||||
|
.generate_key(Rails.application.secrets.encryption_service_salt, ActiveSupport::MessageEncryptor.key_len)
|
||||||
|
legacy_encryptor = ActiveSupport::MessageEncryptor.new(legacy_key)
|
||||||
|
legacy_service.instance_variable_set(:@encryptor, legacy_encryptor)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
# Encrypt the token using the legacy (SHA1) encryption service
|
||||||
|
legacy_encrypted_token = legacy_encryption_service.encrypt(token)
|
||||||
|
procedure.update_column(:encrypted_api_particulier_token, legacy_encrypted_token)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 're-encrypts the api_particulier_token' do
|
||||||
|
old_encrypted_value = procedure.encrypted_api_particulier_token
|
||||||
|
|
||||||
|
expect { subject }.to change { procedure.reload.encrypted_api_particulier_token }
|
||||||
|
expect(procedure.api_particulier_token).to eq(token)
|
||||||
|
|
||||||
|
encrypted_value = procedure.encrypted_api_particulier_token
|
||||||
|
|
||||||
|
# Verify that the new encrypted value can't be decrypted with the legacy service
|
||||||
|
expect { legacy_encryption_service.decrypt(encrypted_value) }
|
||||||
|
.to raise_error(ActiveSupport::MessageEncryptor::InvalidMessage)
|
||||||
|
|
||||||
|
# Verify that the new encrypted value can be decrypted with the current service
|
||||||
|
current_service = EncryptionService.new
|
||||||
|
expect(current_service.decrypt(encrypted_value)).to eq(token)
|
||||||
|
|
||||||
|
# and with the services without rotations
|
||||||
|
current_service = EncryptionService.new
|
||||||
|
current_service.instance_variable_set(:@rotations, [])
|
||||||
|
expect(current_service.decrypt(encrypted_value)).to eq(token)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue