How we upgraded from Rails 6 to Rails 7

Abhishek T

By Abhishek T

on September 20, 2022

Recently, we upgraded all neeto products to Rails 7 using Rails upgrade guide.

Here are the issues we faced during the upgrade.

Migrating to Active Record Encryption

This was the biggest challenge we faced during the upgrade. For encrypting columns, we had used the attr_encrypted gem. However Rails 7 came with Active Record Encryption. So we needed to decrypt the records in the production database and encrypt them using Active Record Encryption. We found that "attr_encrypted" gem was incompatible with Rails 7. So the only option was to remove the "attr_encrypted" gem and decrypt the records using a script. We used the following method to decrypt the records.

1def decrypted_attribute(attribute_name, record)
2  value = record.send(attribute_name)
3  return if value.blank?
4
5  value = Base64.decode64(value)
6
7  cipher = OpenSSL::Cipher.new("aes-256-gcm")
8  cipher.decrypt
9
10  cipher.key = Rails.application.secrets.attr_encrypted[:encryption_key]
11  cipher.iv = Base64.decode64(record.send(:"#{attribute_name}_iv"))
12
13  cipher.auth_tag = value[-16..]
14  cipher.auth_data = ""
15
16  cipher.update(value[0..-17]) + cipher.final
17end

Broken images in Active Storage

After the upgrade, we started getting broken images in some places. This happened for Active Storage links embedded in Rich Text. After some debugging we found that we were getting incorrect Active Storage links because of a change in the key generation algorithm. The following configuration was loading images using the old algorithm.

1config.active_support.key_generator_hash_digest_class = OpenSSL::Digest::SHA1

Since the new algorithm provides more security, we decided to migrate the links instead of using the old algorithm. We used the following code to migrate the old link to the new valid link.

1# Usage:
2# text_with_new_links = ActiveStorageKeyConverter.new(text_with_old_links).process
3# If no links are there to replace, the original text will be returned as it is.
4
5class ActiveStorageKeyConverter
6  def initialize(text)
7    @text = text
8  end
9
10  def process
11    replace(@text)
12  end
13
14  private
15    def convert_key(id)
16      verifier_name = "ActiveStorage"
17      key_generator =  ActiveSupport::KeyGenerator.new(Rails.application.secrets.
18      secret_key_base iterations: 1000, hash_digest_class: OpenSSL::Digest::SHA1)
19      key_generator = ActiveSupport::CachingKeyGenerator.new(key_generator)
20      secret = key_generator.generate_key(verifier_name.to_s)
21      verifier = ActiveSupport::MessageVerifier.new(secret)
22
23      ActiveStorage::Blob.find_by_id(verifier.verify(id, purpose: :blob_id))
24      .try(:signed_id) rescue nil
25    end
26
27    def replace(text)
28      keys = text.scan(URI.regexp).flatten.select{|x|
29      x.to_s.include? ("rails/active_storage")}.map{|x| x.split("/")[-2]}
30      keys.each do |key|
31        new_key = convert_key(key)
32        text = text.gsub(key, new_key) if new_key
33      end
34      text
35    end
36end

Following one time rake task was used to update the Active Storage links in the content column of Task model:

1desc "Update active storage links embedded in rich text to support in rails 7"
2task migrate_old_activestorage_links: :environment do
3
4  table_colum_map = {
5    "Task" => "content",
6  }
7  match_term = "%rails/active_storage%"
8
9  table_colum_map.each do |model_name, column_name|
10    model_name.to_s.constantize.where("#{column_name} ILIKE ?", match_term).find_each do|row|
11      row.update_column(column_name, ActiveStorageKeyConverter.new(row[column_name]).process)
12    end
13  end
14end

Test failures with the mailer jobs

After upgrading to Rails 7, tests related to mailers started to fail. This was because the mailer jobs were enqueued in the default queue instead of mailers. We fixed this by adding the following configuration.

1config.action_mailer.deliver_later_queue_name = :mailers

Autoloading during initialization failed

After the upgrade if we start Rails sever then we were getting the following error.

1$ rails s
2=> Booting Puma
3=> Rails 7.0.3.1 application starting in development
4=> Run `bin/rails server --help` for more startup options
5Exiting
6/Users/BB/Neeto/neeto_commons/lib/neeto_commons/initializers/session_store.rb:13:in
7`session_store': uninitialized constant #<Class:NeetoCommons::Initializers>::ServerSideSession     (NameError)
8
9    ActionDispatch::Session::ActiveRecordStore.session_class = ServerSideSession
10                                                                ^^^^^^^^^^^^^^^^^
11   from /Users/BB/Neeto/neeto-planner-web/config/initializers/common.rb:10:in `<main>'

That error was coming from our internal neeto-commons initializer called session_store.rb. The code looked like this.

1#session_store.rb
2
3module NeetoCommons
4  module Initializers
5    class << self
6      def session_store
7        Rails.application.config.session_store :active_record_store,
8          key: Rails.application.secrets.session_cookie_name, expire_after: 10.years.to_i
9
10        ActiveRecord::SessionStore::Session.table_name = "server_side_sessions"
11        ActiveRecord::SessionStore::Session.primary_key = "session_id"
12        ActiveRecord::SessionStore::Session.serializer = :json
13        ActionDispatch::Session::ActiveRecordStore.session_class = ServerSideSession
14      end
15    end
16  end
17end

In order to fix the issue we had to put the last statement in a block like shown below.

1Rails.application.config.after_initialize do
2 ActionDispatch::Session::ActiveRecordStore.session_class = ServerSideSession
3end

Missing template error with pdf render

After the Rails 7 upgrade the following test started failing.

1def test_get_task_pdf_download_success
2  get api_v1_project_section_tasks_download_path(@project.id, @section, @task, format: :pdf)
3
4  assert_response :ok
5
6  assert response.body.starts_with? "%PDF-1.4"
7  assert response.body.ends_with? "%EOF\n"
8end

The actual error is Missing template api/v1/projects/tasks/show.html.erb.

In order to fix it we renamed the file name from /tasks/show.html.erb to /tasks/show.pdf.erb. Similarly we changed the layout from /layouts/pdf.html.erb to /layouts/pdf.pdf.erb.

Initially the controller code looked like this.

1format.pdf do
2  render \
3    template: "api/v1/projects/tasks/show.html.erb"
4    pdf: pdf_file_name,
5    layout: "pdf.html.erb"
6end

After the change the code looked like this.

1format.pdf do
2  render \
3    template: "api/v1/projects/tasks/show",
4    pdf: pdf_file_name,
5    layout: "pdf"
6end

Open Redirect protection

After the Rails 7 upgrade the following test started failing.

1def test_that_users_are_redirected_to_error_url_when_invalid_subdomain_is_entered
2  invalid_subdomain = "invalid-subdomain"
3
4  auth_subdomain_url = URI(app_secrets.auth_app[:url].gsub(
5   app_secrets.app_subdomain, invalid_subdomain))
6
7  auth_app_url = app_secrets.auth_app[:url]
8
9  host! test_domain(invalid_subdomain)
10  get "/"
11
12  assert_redirected_to auth_app_url
13end

In the test we are expecting the application to redirect to auth_app_url but we are getting UnsafeRedirectError error for open redirections. In Rails 7 the new Rails defaults protects applications against the Open Redirect Vulnerability.

To allow any external redirects we can pass allow_other_host: true.

1redirect_to <External URL>, allow_other_host: true

Since we use open redirection in many places we disabled this protection globally.

1config.action_controller.raise_on_open_redirects = false

Argument Error for Mailgun signing key

After the upgrade, we started getting the following error in production.

1>> ArgumentError: Missing required Mailgun Signing key. Set action_mailbox.mailgun_signing_key
2in your application's encrypted credentials or provide the MAILGUN_INGRESS_SIGNING_KEY
3environment variable.
4

Before Rails 7, we used the MAILGUN_INGRESS_API_KEY environment variable to set up the Mailgun signing key . In Rails 7, that is changed to MAILGUN_INGRESS_SIGNING_KEY. So we renamed the environment variable to fix the problem.