`form_for` is bypassing model accessors. How to make it stop? (Or: How to make a custom attribute serializer?)

前端 未结 3 1731
名媛妹妹
名媛妹妹 2021-01-12 13:14

I set these methods to automatically encrypt values.

class User < ApplicationRecord
  def name=(val)
    super val.encrypt
  end
  def name
    (super()          


        
相关标签:
3条回答
  • 2021-01-12 14:13

    I found this similar question: How do input field methods (text_area, text_field, etc.) get attribute values from a record within a form_for block?

    I added

      def name_before_type_cast
        (super() || '').decrypt
      end
    

    And now it works!

    Here is the full solution:

      @@encrypted_fields = [:name, :phone, :address1, :address2, :ssn, ...]
      @@encrypted_fields.each do |m|
        setter = (m.to_s+'=').to_sym
        getter = m
        getter_btc = (m.to_s+'_before_type_cast').to_sym
        define_method(setter) do |v|
          super v.encrypt
        end
        define_method(getter) do
          (super() || '').decrypt
        end
        define_method(getter_btc) do
          (super() || '').decrypt
        end
      end
    

    Some docs: http://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/BeforeTypeCast.html

    0 讨论(0)
  • 2021-01-12 14:19

    While you can work around this by overloading name_before_type_case, I think this is actually the wrong place to be doing this kind of transformation.

    Based on your example, the requirements here appear to be:

    1. plaintext while in memory
    2. encrypted at rest

    So if we move the encrytion/decryption transformation to the Ruby-DB boundary, this logic becomes much cleaner & reusable.

    Rails 5 introduced a helpful Attributes API for dealing with this exact scenario. Since you have provided no details about how your encryption routine is implemented, I'm going to use Base64 in my example code to demonstrate a text transformation.

    app/types/encrypted_type.rb
    class EncryptedType < ActiveRecord::Type::Text
      # this is called when saving to the DB
      def serialize(value)
        Base64.encode64(value) unless value.nil?
      end
    
      # called when loading from DB
      def deserialize(value)
        Base64.decode64(value) unless value.nil?
      end
    
      # add this if the field is not idempotent
      def changed_in_place?(raw_old_value, new_value)
        deserialize(raw_old_value) != new_value
      end
    end
    
    config/initalizers/types.rb
    ActiveRecord::Type.register(:encrypted, EncryptedType)
    

    Now, you can specify this attribute as encrypted in the model:

    class User < ApplicationRecord
      attribute :name, :encrypted
    
      # If you have a lot of fields, you can use metaprogramming:
      %i[name phone address1 address2 ssn].each do |field_name|
        attribute field_name, :encrypted
      end
    end
    

    The name attribute will be transparently encrypted & decrypted during roundtrips to the DB. This also means that you can apply the same transform to as many attributes as you like without rewriting the same code.

    0 讨论(0)
  • 2021-01-12 14:21

    Why are you exposing it as name at all ?

    class User < ApplicationRecord
        def decrypted_name=(val)
           name = val.encrypt
        end
    
        def decrypted_name
           name.decrypt
        end
    end
    

    Then you use @model.decrypted_name instead of @model.name as name is encrypted, and such saved in DB.

    edit.haml
    =@user.decrypted_name
    =form_for @user, html: { multipart: true } do |f|
      =f.text_field :decrypted_name
    

    And name if it is encrypted should not be handled directly but with this decrypted_name accessor.

    0 讨论(0)
提交回复
热议问题