DmitryTsepelev / store_model

Work with JSON-backed attributes as ActiveRecord-ish models
MIT License
1.04k stars 85 forks source link

Serializing and deserializing values #146

Closed RudskikhIvan closed 1 year ago

RudskikhIvan commented 1 year ago

Hello,

In our project we need to encrypt some data stored through store_model. But it doesn't work out from the box, because store_model doesn't call serialize method for attributes. This PR is possible solution. There is breaking change, because now datetime and time uses serialization from types and stores without timezone. What do you think?

DmitryTsepelev commented 1 year ago

Hey hey! Are you using Rails encryption? I know https://github.com/ankane/lockbox works fine with the current implementation

RudskikhIvan commented 1 year ago

We use lockbox. But looks like it doesn't work with store_model.

Example:

class AccessToken < Authorization
  class Config
    include StoreModel::Model
    extend Lockbox::Model

    attribute :access_token_ciphertext, :string
    has_encrypted :access_token
  end

  attribute :config, Config.to_type
end

It fails with:

NameError:
  undefined method `access_token_ciphertext_changed?' for class `AccessToken::Config'

              alias_method "#{name}_changed?", "#{encrypted_attribute}_changed?"
              ^^^^^^^^^^^^
# /Users/ivanrudskikh/.rvm/gems/ruby-3.1.3/gems/lockbox-1.2.0/lib/lockbox/model.rb:402:in `alias_method'
# /Users/ivanrudskikh/.rvm/gems/ruby-3.1.3/gems/lockbox-1.2.0/lib/lockbox/model.rb:402:in `block (2 levels) in has_encrypted'
# /Users/ivanrudskikh/.rvm/gems/ruby-3.1.3/gems/lockbox-1.2.0/lib/lockbox/model.rb:60:in `class_eval'
# /Users/ivanrudskikh/.rvm/gems/ruby-3.1.3/gems/lockbox-1.2.0/lib/lockbox/model.rb:60:in `block in has_encrypted'
# /Users/ivanrudskikh/.rvm/gems/ruby-3.1.3/gems/lockbox-1.2.0/lib/lockbox/model.rb:39:in `each'
# /Users/ivanrudskikh/.rvm/gems/ruby-3.1.3/gems/lockbox-1.2.0/lib/lockbox/model.rb:39:in `has_encrypted'
RudskikhIvan commented 1 year ago

Anyway, current implementation doesn't use ActiveModel::Types in full.

DmitryTsepelev commented 1 year ago

Looks like there's something broken in the lockbox: there's a similar issue with the similar gem https://github.com/ankane/lockbox/issues/174. Author suggests to do something like:

class AccessToken < Authorization
  class Config
    include StoreModel::Model
    extend Lockbox::Model

    attribute :access_token, :string
    has_encrypted :access_token
  end

  attribute :config, Config.to_type
end

i.e. attribute and has_encrypted get the same attribute name (but the column is called access_token_ciphertext in the table). Could you please try it out?

RudskikhIvan commented 1 year ago

The same exception:

NameError:
  undefined method `access_token_ciphertext_changed?' for class `AccessToken::Config'

              alias_method "#{name}_changed?", "#{encrypted_attribute}_changed?"
DmitryTsepelev commented 1 year ago

Got it 😞Let's give it a shot, I'll make a major release at the end of the week, thank you for handling that!

colszowka commented 1 year ago

Hi, I think this change broke the handling of defaults, I submitted a reproducible example over at #147

balbesina commented 1 year ago

We use lockbox. But looks like it doesn't work with store_model.

store_model has no active_model/dirty included. did you try to include it or define missing methods explicitly?

class Config
  include StoreModel::Model
  include ActiveModel::Dirty
  # ...
end
mweitzel commented 1 year ago

I'm using Lockbox's documentation for strings encryption, paired with a ActiveModel::Type::Value for serialize/deserialize. It works pretty well. Something like this:

class Lockboxify < ActiveModel::Type::Value
  def serialize(value)
    return unless value.present?
    lockbox = Lockbox.new(key: Lockbox.master_key, encode: true)
    lockbox.encrypt(value)
  end

  def deserialize(value)
    return unless value.present?
    lockbox = Lockbox.new(key: Lockbox.master_key, encode: true)
    lockbox.decrypt(value)
  end
end

and then use like

class Something < Anything
  class Store
    include StoreModel::Model

    attribute :token, Lockboxify.new
  end

  attribute :store, Store.to_type, default: -> { {} }
end