Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AR encryption breaks store #43012

Closed
georgeclaghorn opened this issue Aug 14, 2021 · 3 comments · Fixed by #43013
Closed

AR encryption breaks store #43012

georgeclaghorn opened this issue Aug 14, 2021 · 3 comments · Fixed by #43013

Comments

@georgeclaghorn
Copy link
Contributor

georgeclaghorn commented Aug 14, 2021

Reproduction script

Click to expand…
# frozen_string_literal: true

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  gem "rails", github: "rails/rails", branch: "main"
  gem "sqlite3"
end

require "active_record"
require "minitest/autorun"
require "logger"

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

# Example credentials from AR encryption guide:
ActiveRecord::Encryption.configure \
  primary_key: "EGY8WhulUOXixybod7ZWwMIL68R9o5kC",
  deterministic_key: "aPA5XyALhf75NNnMzaspW7akTfZp0lPY",
  key_derivation_salt: "xEY0dt6TZcAMg52K7O84wYzkjvbA62Hz"

ActiveRecord::Schema.define do
  create_table :foos, force: true do |t|
    t.text :data
  end
end

class Foo < ActiveRecord::Base
  store :data, accessors: %i[ stuff ]
  encrypts :data
end

class BugTest < Minitest::Test
  def test_create_with_store_accessor
    Foo.create! stuff: "stuff"
    assert_equal "stuff", Foo.last.stuff
  end
end

Expected behavior

A store attribute can be encrypted. Stored values can be read and written via accessors.

Actual behavior

When attempting to read or write through a store accessor, an exception is raised:

NoMethodError: undefined method `accessor' for #<ActiveRecord::Encryption::EncryptedAttributeType:0x0000000156b115c8 @true="t", @false="f", @precision=nil, @scale=nil, @limit=nil, @scheme=#<ActiveRecord::Encryption::Scheme:0x00000001304a3888 @key_provider_param=nil, @key=nil, @deterministic=false, @downcase=false, @ignore_case=false, @previous_schemes_param=nil, @previous_schemes=[], @context_properties={}>, @cast_type=#<ActiveRecord::Type::Serialized:0x0000000156b89d20 @subtype=#<ActiveRecord::Type::Text:0x0000000156b98898 @true="t", @false="f", @precision=nil, @scale=nil, @limit=nil>, @coder=#<ActiveRecord::Store::IndifferentCoder:0x00000001304b14b0 @coder=#<ActiveRecord::Coders::YAMLColumn:0x00000001304ab600 @attr_name=:data, @object_class=Object>>, @delegate_dc_obj=#<ActiveRecord::Type::Text:0x0000000156b98898 @true="t", @false="f", @precision=nil, @scale=nil, @limit=nil>>, @previous_type=false>
    /Users/george/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/bundler/gems/rails-6ec669b65d5c/activerecord/lib/active_record/store.rb:217:in `store_accessor_for'
    /Users/george/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/bundler/gems/rails-6ec669b65d5c/activerecord/lib/active_record/store.rb:212:in `write_store_attribute'
    /Users/george/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/bundler/gems/rails-6ec669b65d5c/activerecord/lib/active_record/store.rb:136:in `block (3 levels) in store_accessor'
    /Users/george/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/bundler/gems/rails-6ec669b65d5c/activemodel/lib/active_model/attribute_assignment.rb:49:in `public_send'
    /Users/george/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/bundler/gems/rails-6ec669b65d5c/activemodel/lib/active_model/attribute_assignment.rb:49:in `_assign_attribute'
    /Users/george/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/bundler/gems/rails-6ec669b65d5c/activerecord/lib/active_record/attribute_assignment.rb:21:in `block in _assign_attributes'
    /Users/george/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/bundler/gems/rails-6ec669b65d5c/activerecord/lib/active_record/attribute_assignment.rb:13:in `each'
    /Users/george/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/bundler/gems/rails-6ec669b65d5c/activerecord/lib/active_record/attribute_assignment.rb:13:in `_assign_attributes'
    /Users/george/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/bundler/gems/rails-6ec669b65d5c/activemodel/lib/active_model/attribute_assignment.rb:34:in `assign_attributes'
    /Users/george/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/bundler/gems/rails-6ec669b65d5c/activerecord/lib/active_record/core.rb:479:in `initialize'
    /Users/george/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/bundler/gems/rails-6ec669b65d5c/activerecord/lib/active_record/inheritance.rb:74:in `new'
    /Users/george/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/bundler/gems/rails-6ec669b65d5c/activerecord/lib/active_record/inheritance.rb:74:in `new'
    /Users/george/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/bundler/gems/rails-6ec669b65d5c/activerecord/lib/active_record/persistence.rb:54:in `create!'
    bug.rb:45:in `test_create_with_store_accessor'

System configuration

Rails version: Latest main
Ruby version: 3.0.1

/cc @jorgemanrubia

@georgeclaghorn georgeclaghorn changed the title AR encryption breaks store_accessor AR encryption breaks store Aug 14, 2021
@georgeclaghorn
Copy link
Contributor Author

I think this method just needs to be updated to account for ActiveRecord::Encryption::EncryptedAttributeType:

def store_accessor_for(store_attribute)
type_for_attribute(store_attribute).accessor
end

Something like this?

def store_accessor_for(store_attribute)
  type_for_attribute_before_encryption(store_attribute).accessor
end

def type_for_attribute_before_encryption(name)
  type_for_attribute(name).then { |type| type.try(:cast_type) || type }
end

@jorgemanrubia
Copy link
Contributor

Thanks for the great report @georgeclaghorn 🙏. Fix in #43013. I think I prefer to delegate #accessor to cast_type versus including an exception for encrypted attributes in the store-attribute code.

@joesiewert
Copy link

@jorgemanrubia @georgeclaghorn I think I'm running into a similar issue over on hashicorp/vault-rails#138.

Any ideas on how I might fix or workaround that issue? Much appreciated if you happen to have a moment to look. 🙇

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants