Module: BlindIndex::Model

Defined in:
lib/blind_index/model.rb

Defined Under Namespace

Modules: InstanceMethods

Instance Method Summary collapse

Instance Method Details

#blind_index(*attributes, rotate: false, migrating: false, **opts) ⇒ Object



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/blind_index/model.rb', line 3

def blind_index(*attributes, rotate: false, migrating: false, **opts)
  indexes = attributes.map { |a| [a, opts.dup] }
  indexes.concat(attributes.map { |a| [a, rotate.merge(rotate: true)] }) if rotate

  indexes.each do |name, options|
    rotate = options.delete(:rotate)

    # check here so we validate rotate options as well
    unknown_keywords = options.keys - [:algorithm, :attribute, :bidx_attribute,
      :callback, :cost, :encode, :expression, :insecure_key, :iterations, :key,
      :key_attribute, :key_table, :legacy, :master_key, :size, :slow, :version]
    raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?

    attribute = options[:attribute] || name
    version = (options[:version] || 1).to_i
    callback = options[:callback].nil? ? true : options[:callback]
    if options[:bidx_attribute]
      bidx_attribute = options[:bidx_attribute]
    else
      bidx_attribute = name
      bidx_attribute = "encrypted_#{bidx_attribute}" if options[:legacy]
      bidx_attribute = "#{bidx_attribute}_bidx"
      bidx_attribute = "#{bidx_attribute}_v#{version}" if version != 1
    end

    name = "migrated_#{name}" if migrating
    name = "rotated_#{name}" if rotate
    name = name.to_sym
    attribute = attribute.to_sym
    method_name = :"compute_#{name}_bidx"
    class_method_name = :"generate_#{name}_bidx"

    key = options[:key]
    key ||= -> { BlindIndex.index_key(table: options[:key_table] || try(:table_name) || collection_name.to_s, bidx_attribute: options[:key_attribute] || bidx_attribute, master_key: options[:master_key], encode: false) }

    class_eval do
      activerecord = defined?(ActiveRecord) && self < ActiveRecord::Base

      if activerecord && ActiveRecord::VERSION::MAJOR >= 6
        # blind index value isn't really sensitive
        # but don't need to show it in the Rails console
        self.filter_attributes += [/\A#{Regexp.escape(bidx_attribute)}\z/]
      end

      @blind_indexes ||= {}

      unless respond_to?(:blind_indexes)
        def self.blind_indexes
          parent_indexes =
            if superclass.respond_to?(:blind_indexes)
              superclass.blind_indexes
            else
              {}
            end

          parent_indexes.merge(@blind_indexes || {})
        end
      end

      raise BlindIndex::Error, "Duplicate blind index: #{name}" if blind_indexes[name]

      @blind_indexes[name] = options.merge(
        key: key,
        attribute: attribute,
        bidx_attribute: bidx_attribute,
        migrating: migrating
      )

      define_singleton_method class_method_name do |value|
        BlindIndex.generate_bidx(value, **blind_indexes[name])
      end

      define_method method_name do
        send("#{bidx_attribute}=", self.class.send(class_method_name, send(attribute)))
      end

      if callback
        # TODO reuse module
        m = Module.new do
          define_method "#{attribute}=" do |value|
            result = super(value)
            send(method_name)
            result
          end

          unless activerecord
            define_method "reset_#{attribute}!" do
              result = super()
              send(method_name)
              result
            end
          end
        end
        prepend m
      end

      # use include so user can override
      include InstanceMethods if blind_indexes.size == 1
    end
  end
end